@jant/core 0.3.35 → 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/module-RjUF93sV.js +716 -0
- package/dist/client/assets/native-48B9X9Wg.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4564 -3013
- package/dist/index.js +12885 -8161
- package/package.json +23 -6
- package/src/__tests__/helpers/app.ts +10 -10
- package/src/__tests__/helpers/db.ts +91 -87
- package/src/app.tsx +157 -31
- 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/{lib → client}/avatar-upload.ts +4 -3
- package/src/{lib → client}/collection-form-bridge.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-collection-form.test.ts +26 -9
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1140 -0
- package/src/client/components/__tests__/jant-compose-editor.test.ts +504 -0
- package/src/{ui → client}/components/__tests__/jant-post-form.test.ts +37 -17
- package/src/{ui → client}/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/{ui → client}/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +43 -0
- package/src/{ui → client}/components/collection-types.ts +3 -4
- package/src/client/components/compose-types.ts +174 -0
- package/src/client/components/jant-collection-form.ts +667 -0
- package/src/client/components/jant-collection-sidebar.ts +805 -0
- package/src/client/components/jant-compose-dialog.ts +2161 -0
- package/src/client/components/jant-compose-editor.ts +1813 -0
- package/src/client/components/jant-compose-fullscreen.ts +283 -0
- package/src/client/components/jant-media-lightbox.ts +259 -0
- package/src/{ui → client}/components/jant-nav-manager.ts +97 -298
- package/src/{ui → client}/components/jant-post-form.ts +141 -12
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/{ui → client}/components/jant-settings-avatar.ts +3 -3
- package/src/{ui → client}/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/{ui → client}/components/nav-manager-types.ts +6 -18
- package/src/{ui → client}/components/post-form-template.ts +137 -38
- package/src/{ui → client}/components/post-form-types.ts +15 -4
- package/src/client/compose-bridge.ts +583 -0
- package/src/{lib → client}/image-processor.ts +26 -8
- package/src/client/lazy-slugify.ts +51 -0
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/{lib → client}/nav-manager-bridge.ts +1 -1
- package/src/{lib → client}/post-form-bridge.ts +53 -2
- package/src/{lib → client}/settings-bridge.ts +3 -15
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/bubble-menu.ts +205 -0
- package/src/client/tiptap/create-editor.ts +86 -0
- package/src/client/tiptap/exitable-marks.ts +73 -0
- package/src/client/tiptap/extensions.ts +65 -0
- package/src/client/tiptap/image-node.ts +482 -0
- package/src/client/tiptap/link-toolbar.ts +371 -0
- package/src/client/tiptap/more-break.ts +50 -0
- package/src/client/tiptap/paste-image.ts +129 -0
- package/src/client/tiptap/slash-commands.ts +438 -0
- package/src/{lib → client}/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +44 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +27 -17
- 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 -140
- 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 +783 -1087
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +867 -812
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +878 -823
- 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__/resolve-config.test.ts +2 -2
- package/src/lib/__tests__/schemas.test.ts +186 -65
- 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__/url.test.ts +2 -2
- package/src/lib/__tests__/view.test.ts +140 -65
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +963 -0
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +77 -31
- 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 +22 -12
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +24 -5
- package/src/lib/resolve-config.ts +13 -2
- package/src/lib/schemas.ts +226 -58
- 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 +158 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +76 -34
- package/src/lib/tiptap-render.ts +191 -0
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +263 -14
- package/src/lib/url.ts +37 -22
- package/src/lib/view.ts +236 -55
- 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/error-handler.ts +3 -3
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +83 -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 +57 -31
- 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 +81 -62
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +92 -24
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/reset.tsx +5 -4
- package/src/routes/auth/setup.tsx +39 -31
- package/src/routes/auth/signin.tsx +13 -14
- package/src/routes/compose.tsx +27 -63
- package/src/routes/dash/__tests__/settings-avatar.test.ts +44 -9
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +475 -99
- package/src/routes/feed/__tests__/rss.test.ts +22 -23
- package/src/routes/feed/rss.ts +6 -2
- 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 +36 -18
- package/src/routes/pages/archive.tsx +177 -37
- package/src/routes/pages/collection.tsx +43 -14
- package/src/routes/pages/collections.tsx +11 -2
- package/src/routes/pages/featured.tsx +27 -3
- package/src/routes/pages/home.tsx +15 -14
- 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 +800 -230
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/__tests__/settings.test.ts +3 -3
- 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 +764 -172
- package/src/services/search.ts +161 -74
- package/src/services/settings.ts +6 -2
- package/src/styles/components.css +293 -62
- package/src/styles/tokens.css +93 -5
- package/src/styles/ui.css +4349 -766
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +34 -4
- package/src/types/constants.ts +17 -2
- package/src/types/entities.ts +83 -37
- package/src/types/operations.ts +20 -27
- package/src/types/props.ts +52 -17
- package/src/types/views.ts +48 -24
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +255 -16
- 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 +12 -2
- package/src/ui/dash/appearance/AdvancedContent.tsx +71 -59
- package/src/ui/dash/appearance/ColorThemeContent.tsx +48 -33
- package/src/ui/dash/appearance/FontThemeContent.tsx +65 -73
- package/src/ui/dash/appearance/NavigationContent.tsx +106 -135
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +87 -146
- 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 +78 -0
- package/src/ui/dash/settings/GeneralContent.tsx +3 -62
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +266 -0
- 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 +116 -103
- package/src/ui/pages/ArchivePage.tsx +923 -95
- package/src/ui/pages/CollectionPage.tsx +6 -35
- package/src/ui/pages/CollectionsPage.tsx +2 -1
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/FeaturedPage.tsx +2 -1
- package/src/ui/pages/HomePage.tsx +1 -1
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +182 -38
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +239 -4
- package/src/ui/shared/MediaGallery.tsx +475 -41
- 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/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/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/collections-reorder.ts +0 -28
- package/src/lib/compose-bridge.ts +0 -280
- package/src/lib/media-upload.ts +0 -148
- 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/appearance.tsx +0 -240
- package/src/routes/dash/collections.tsx +0 -211
- package/src/routes/dash/index.tsx +0 -103
- package/src/routes/dash/media.tsx +0 -132
- package/src/routes/dash/pages.tsx +0 -239
- package/src/routes/dash/posts.tsx +0 -334
- package/src/routes/dash/redirects.tsx +0 -257
- 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 -203
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/types/sortablejs.d.ts +0 -29
- package/src/ui/components/__tests__/jant-compose-dialog.test.ts +0 -512
- package/src/ui/components/__tests__/jant-compose-editor.test.ts +0 -272
- package/src/ui/components/compose-types.ts +0 -75
- package/src/ui/components/jant-collection-form.ts +0 -512
- package/src/ui/components/jant-compose-dialog.ts +0 -495
- package/src/ui/components/jant-compose-editor.ts +0 -814
- package/src/ui/dash/PageForm.tsx +0 -185
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/appearance/AppearanceNav.tsx +0 -60
- package/src/ui/dash/collections/CollectionForm.tsx +0 -166
- package/src/ui/dash/collections/CollectionsListContent.tsx +0 -146
- package/src/ui/dash/collections/IconPickerGrid.tsx +0 -50
- package/src/ui/dash/collections/ViewCollectionContent.tsx +0 -103
- package/src/ui/dash/media/MediaListContent.tsx +0 -201
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -74
- package/src/ui/dash/posts/PostForm.tsx +0 -248
- package/src/ui/dash/settings/SettingsNav.tsx +0 -52
- package/src/ui/layouts/DashLayout.tsx +0 -165
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
- /package/src/{ui → client}/components/settings-types.ts +0 -0
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dashboard Redirects Routes
|
|
3
|
-
*
|
|
4
|
-
* Mounted under /dash/settings/redirects
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { Hono } from "hono";
|
|
8
|
-
import { z } from "zod";
|
|
9
|
-
import { useLingui } from "@lingui/react/macro";
|
|
10
|
-
import type { Bindings, Redirect } from "../../types.js";
|
|
11
|
-
import type { AppVariables } from "../../types/app-context.js";
|
|
12
|
-
import { DashLayout } from "../../ui/layouts/DashLayout.js";
|
|
13
|
-
import { EmptyState, ListItemRow, ActionButtons } from "../../ui/dash/index.js";
|
|
14
|
-
import { SettingsNav } from "../../ui/dash/settings/SettingsNav.js";
|
|
15
|
-
import { dsRedirect } from "../../lib/sse.js";
|
|
16
|
-
import { RedirectTypeSchema, parseValidated } from "../../lib/schemas.js";
|
|
17
|
-
|
|
18
|
-
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
19
|
-
|
|
20
|
-
const CreateRedirectBody = z.object({
|
|
21
|
-
fromPath: z.string().min(1),
|
|
22
|
-
toPath: z.string().min(1),
|
|
23
|
-
type: RedirectTypeSchema,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
export const redirectsRoutes = new Hono<Env>();
|
|
27
|
-
|
|
28
|
-
function RedirectsListContent({ redirects }: { redirects: Redirect[] }) {
|
|
29
|
-
const { t } = useLingui();
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<>
|
|
33
|
-
<SettingsNav currentTab="redirects" />
|
|
34
|
-
|
|
35
|
-
<div class="flex items-center justify-between mb-6">
|
|
36
|
-
<h2 class="text-lg font-medium">
|
|
37
|
-
{t({
|
|
38
|
-
message: "Redirects",
|
|
39
|
-
comment: "@context: Settings section heading",
|
|
40
|
-
})}
|
|
41
|
-
</h2>
|
|
42
|
-
<a href="/dash/settings/redirects/new" class="btn">
|
|
43
|
-
{t({
|
|
44
|
-
message: "New Redirect",
|
|
45
|
-
comment: "@context: Button to create new redirect",
|
|
46
|
-
})}
|
|
47
|
-
</a>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
{redirects.length === 0 ? (
|
|
51
|
-
<EmptyState
|
|
52
|
-
message={t({
|
|
53
|
-
message: "No redirects configured.",
|
|
54
|
-
comment: "@context: Empty state message",
|
|
55
|
-
})}
|
|
56
|
-
ctaText={t({
|
|
57
|
-
message: "New Redirect",
|
|
58
|
-
comment: "@context: Button to create new redirect",
|
|
59
|
-
})}
|
|
60
|
-
ctaHref="/dash/settings/redirects/new"
|
|
61
|
-
/>
|
|
62
|
-
) : (
|
|
63
|
-
<div class="flex flex-col divide-y">
|
|
64
|
-
{redirects.map((r) => (
|
|
65
|
-
<ListItemRow
|
|
66
|
-
key={r.id}
|
|
67
|
-
actions={
|
|
68
|
-
<ActionButtons
|
|
69
|
-
deleteAction={`/dash/settings/redirects/${r.id}/delete`}
|
|
70
|
-
deleteLabel={t({
|
|
71
|
-
message: "Delete",
|
|
72
|
-
comment: "@context: Button to delete redirect",
|
|
73
|
-
})}
|
|
74
|
-
/>
|
|
75
|
-
}
|
|
76
|
-
>
|
|
77
|
-
<div class="flex items-center gap-2">
|
|
78
|
-
<code class="text-sm bg-muted px-1 rounded">{r.fromPath}</code>
|
|
79
|
-
<span class="text-muted-foreground">→</span>
|
|
80
|
-
<code class="text-sm bg-muted px-1 rounded">{r.toPath}</code>
|
|
81
|
-
<span class="badge-outline">{r.type}</span>
|
|
82
|
-
</div>
|
|
83
|
-
</ListItemRow>
|
|
84
|
-
))}
|
|
85
|
-
</div>
|
|
86
|
-
)}
|
|
87
|
-
</>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function NewRedirectContent() {
|
|
92
|
-
const { t } = useLingui();
|
|
93
|
-
|
|
94
|
-
return (
|
|
95
|
-
<>
|
|
96
|
-
<SettingsNav currentTab="redirects" />
|
|
97
|
-
|
|
98
|
-
<h2 class="text-lg font-medium mb-6">
|
|
99
|
-
{t({ message: "New Redirect", comment: "@context: Page heading" })}
|
|
100
|
-
</h2>
|
|
101
|
-
|
|
102
|
-
<form
|
|
103
|
-
data-signals="{fromPath: '', toPath: '', type: '301'}"
|
|
104
|
-
data-on:submit__prevent="@post('/dash/settings/redirects')"
|
|
105
|
-
data-indicator="_loading"
|
|
106
|
-
class="flex flex-col gap-4 max-w-lg"
|
|
107
|
-
>
|
|
108
|
-
<div class="field">
|
|
109
|
-
<label class="label">
|
|
110
|
-
{t({
|
|
111
|
-
message: "From Path",
|
|
112
|
-
comment: "@context: Redirect form field",
|
|
113
|
-
})}
|
|
114
|
-
</label>
|
|
115
|
-
<input
|
|
116
|
-
type="text"
|
|
117
|
-
data-bind="fromPath"
|
|
118
|
-
class="input"
|
|
119
|
-
placeholder="/old-path"
|
|
120
|
-
required
|
|
121
|
-
/>
|
|
122
|
-
<p class="text-xs text-muted-foreground mt-1">
|
|
123
|
-
{t({
|
|
124
|
-
message: "The path to redirect from",
|
|
125
|
-
comment: "@context: Redirect from path help text",
|
|
126
|
-
})}
|
|
127
|
-
</p>
|
|
128
|
-
</div>
|
|
129
|
-
|
|
130
|
-
<div class="field">
|
|
131
|
-
<label class="label">
|
|
132
|
-
{t({
|
|
133
|
-
message: "To Path",
|
|
134
|
-
comment: "@context: Redirect form field",
|
|
135
|
-
})}
|
|
136
|
-
</label>
|
|
137
|
-
<input
|
|
138
|
-
type="text"
|
|
139
|
-
data-bind="toPath"
|
|
140
|
-
class="input"
|
|
141
|
-
placeholder="/new-path or https://..."
|
|
142
|
-
required
|
|
143
|
-
/>
|
|
144
|
-
<p class="text-xs text-muted-foreground mt-1">
|
|
145
|
-
{t({
|
|
146
|
-
message: "The destination path or URL",
|
|
147
|
-
comment: "@context: Redirect to path help text",
|
|
148
|
-
})}
|
|
149
|
-
</p>
|
|
150
|
-
</div>
|
|
151
|
-
|
|
152
|
-
<div class="field">
|
|
153
|
-
<label class="label">
|
|
154
|
-
{t({ message: "Type", comment: "@context: Redirect form field" })}
|
|
155
|
-
</label>
|
|
156
|
-
<select data-bind="type" class="select">
|
|
157
|
-
<option value="301">
|
|
158
|
-
{t({
|
|
159
|
-
message: "301 (Permanent)",
|
|
160
|
-
comment: "@context: Redirect type option",
|
|
161
|
-
})}
|
|
162
|
-
</option>
|
|
163
|
-
<option value="302">
|
|
164
|
-
{t({
|
|
165
|
-
message: "302 (Temporary)",
|
|
166
|
-
comment: "@context: Redirect type option",
|
|
167
|
-
})}
|
|
168
|
-
</option>
|
|
169
|
-
</select>
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
<div class="flex gap-2">
|
|
173
|
-
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
174
|
-
<svg
|
|
175
|
-
data-show="$_loading"
|
|
176
|
-
style="display:none"
|
|
177
|
-
class="animate-spin size-4"
|
|
178
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
179
|
-
viewBox="0 0 24 24"
|
|
180
|
-
fill="none"
|
|
181
|
-
stroke="currentColor"
|
|
182
|
-
stroke-width="2"
|
|
183
|
-
stroke-linecap="round"
|
|
184
|
-
stroke-linejoin="round"
|
|
185
|
-
role="status"
|
|
186
|
-
>
|
|
187
|
-
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
188
|
-
</svg>
|
|
189
|
-
{t({
|
|
190
|
-
message: "Create Redirect",
|
|
191
|
-
comment: "@context: Button to save new redirect",
|
|
192
|
-
})}
|
|
193
|
-
</button>
|
|
194
|
-
<a href="/dash/settings/redirects" class="btn-outline">
|
|
195
|
-
{t({
|
|
196
|
-
message: "Cancel",
|
|
197
|
-
comment: "@context: Button to cancel form",
|
|
198
|
-
})}
|
|
199
|
-
</a>
|
|
200
|
-
</div>
|
|
201
|
-
</form>
|
|
202
|
-
</>
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// List redirects
|
|
207
|
-
redirectsRoutes.get("/", async (c) => {
|
|
208
|
-
const siteName = c.var.appConfig.siteName;
|
|
209
|
-
const redirects = await c.var.services.redirects.list();
|
|
210
|
-
|
|
211
|
-
return c.html(
|
|
212
|
-
<DashLayout
|
|
213
|
-
c={c}
|
|
214
|
-
title="Settings"
|
|
215
|
-
siteName={siteName}
|
|
216
|
-
currentPath="/dash/settings"
|
|
217
|
-
>
|
|
218
|
-
<RedirectsListContent redirects={redirects} />
|
|
219
|
-
</DashLayout>,
|
|
220
|
-
);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
// New redirect form
|
|
224
|
-
redirectsRoutes.get("/new", async (c) => {
|
|
225
|
-
const siteName = c.var.appConfig.siteName;
|
|
226
|
-
|
|
227
|
-
return c.html(
|
|
228
|
-
<DashLayout
|
|
229
|
-
c={c}
|
|
230
|
-
title="Settings"
|
|
231
|
-
siteName={siteName}
|
|
232
|
-
currentPath="/dash/settings"
|
|
233
|
-
>
|
|
234
|
-
<NewRedirectContent />
|
|
235
|
-
</DashLayout>,
|
|
236
|
-
);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// Create redirect
|
|
240
|
-
redirectsRoutes.post("/", async (c) => {
|
|
241
|
-
const body = parseValidated(CreateRedirectBody, await c.req.json());
|
|
242
|
-
|
|
243
|
-
const type = parseInt(body.type, 10) as 301 | 302;
|
|
244
|
-
await c.var.services.redirects.create(body.fromPath, body.toPath, type);
|
|
245
|
-
|
|
246
|
-
return dsRedirect("/dash/settings/redirects");
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
// Delete redirect
|
|
250
|
-
redirectsRoutes.post("/:id/delete", async (c) => {
|
|
251
|
-
const id = parseInt(c.req.param("id"), 10);
|
|
252
|
-
if (!isNaN(id)) {
|
|
253
|
-
await c.var.services.redirects.delete(id);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return dsRedirect("/dash/settings/redirects");
|
|
257
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single Post Page Route
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { Hono } from "hono";
|
|
6
|
-
import type { Bindings } from "../../types.js";
|
|
7
|
-
import type { AppVariables } from "../../types/app-context.js";
|
|
8
|
-
import { PostPage } from "../../ui/pages/PostPage.js";
|
|
9
|
-
import * as sqid from "../../lib/sqid.js";
|
|
10
|
-
import { getNavigationData } from "../../lib/navigation.js";
|
|
11
|
-
import { renderPublicPage } from "../../lib/render.js";
|
|
12
|
-
import { buildMediaMap } from "../../lib/media-helpers.js";
|
|
13
|
-
import { createMediaContext, toPostView } from "../../lib/view.js";
|
|
14
|
-
|
|
15
|
-
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
16
|
-
|
|
17
|
-
export const postRoutes = new Hono<Env>();
|
|
18
|
-
|
|
19
|
-
postRoutes.get("/:id", async (c) => {
|
|
20
|
-
const paramId = c.req.param("id");
|
|
21
|
-
|
|
22
|
-
// Decode sqid to numeric ID
|
|
23
|
-
const id = sqid.decode(paramId);
|
|
24
|
-
if (!id) return c.notFound();
|
|
25
|
-
|
|
26
|
-
const post = await c.var.services.posts.getById(id);
|
|
27
|
-
if (!post) return c.notFound();
|
|
28
|
-
|
|
29
|
-
// Don't show drafts on public site
|
|
30
|
-
if (post.status === "draft") {
|
|
31
|
-
return c.notFound();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Batch load media attachments
|
|
35
|
-
const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
|
|
36
|
-
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
37
|
-
const mediaMap = buildMediaMap(
|
|
38
|
-
rawMediaMap,
|
|
39
|
-
mediaCtx.r2PublicUrl,
|
|
40
|
-
mediaCtx.imageTransformUrl,
|
|
41
|
-
mediaCtx.s3PublicUrl,
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
// Transform to View Model
|
|
45
|
-
const postView = toPostView(
|
|
46
|
-
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
47
|
-
mediaCtx,
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
const navData = await getNavigationData(c);
|
|
51
|
-
const title = post.title || navData.siteName;
|
|
52
|
-
|
|
53
|
-
return renderPublicPage(c, {
|
|
54
|
-
title,
|
|
55
|
-
description: post.body?.slice(0, 160),
|
|
56
|
-
navData,
|
|
57
|
-
content: <PostPage post={postView} />,
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
3
|
-
import { createPageService } from "../page.js";
|
|
4
|
-
import { createPostService } from "../post.js";
|
|
5
|
-
import { createNavItemService } from "../navigation.js";
|
|
6
|
-
import { createPathRegistryService } from "../path-registry.js";
|
|
7
|
-
import { ValidationError, ConflictError } from "../../lib/errors.js";
|
|
8
|
-
import type { Database } from "../../db/index.js";
|
|
9
|
-
|
|
10
|
-
describe("PageService", () => {
|
|
11
|
-
let db: Database;
|
|
12
|
-
let pageService: ReturnType<typeof createPageService>;
|
|
13
|
-
let postService: ReturnType<typeof createPostService>;
|
|
14
|
-
let navItemService: ReturnType<typeof createNavItemService>;
|
|
15
|
-
let pathRegistry: ReturnType<typeof createPathRegistryService>;
|
|
16
|
-
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
const testDb = createTestDatabase();
|
|
19
|
-
db = testDb.db as unknown as Database;
|
|
20
|
-
pathRegistry = createPathRegistryService(db);
|
|
21
|
-
pageService = createPageService(db, pathRegistry);
|
|
22
|
-
postService = createPostService(db, pathRegistry);
|
|
23
|
-
navItemService = createNavItemService(db);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe("listNotInNav", () => {
|
|
27
|
-
it("returns all pages when none are in navigation", async () => {
|
|
28
|
-
await pageService.create({ slug: "about", title: "About" });
|
|
29
|
-
await pageService.create({ slug: "contact", title: "Contact" });
|
|
30
|
-
|
|
31
|
-
const pages = await pageService.listNotInNav();
|
|
32
|
-
expect(pages).toHaveLength(2);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("excludes pages that have a nav item", async () => {
|
|
36
|
-
const aboutPage = await pageService.create({
|
|
37
|
-
slug: "about",
|
|
38
|
-
title: "About",
|
|
39
|
-
});
|
|
40
|
-
await pageService.create({ slug: "contact", title: "Contact" });
|
|
41
|
-
|
|
42
|
-
// Add "About" to navigation
|
|
43
|
-
await navItemService.create({
|
|
44
|
-
type: "page",
|
|
45
|
-
label: "About",
|
|
46
|
-
url: "/about",
|
|
47
|
-
pageId: aboutPage.id,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const pages = await pageService.listNotInNav();
|
|
51
|
-
expect(pages).toHaveLength(1);
|
|
52
|
-
expect(pages[0]?.slug).toBe("contact");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("returns empty array when all pages are in navigation", async () => {
|
|
56
|
-
const aboutPage = await pageService.create({
|
|
57
|
-
slug: "about",
|
|
58
|
-
title: "About",
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
await navItemService.create({
|
|
62
|
-
type: "page",
|
|
63
|
-
label: "About",
|
|
64
|
-
url: "/about",
|
|
65
|
-
pageId: aboutPage.id,
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const pages = await pageService.listNotInNav();
|
|
69
|
-
expect(pages).toHaveLength(0);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("returns empty array when no pages exist", async () => {
|
|
73
|
-
const pages = await pageService.listNotInNav();
|
|
74
|
-
expect(pages).toHaveLength(0);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("is not affected by link-type nav items (no pageId)", async () => {
|
|
78
|
-
await pageService.create({ slug: "about", title: "About" });
|
|
79
|
-
|
|
80
|
-
// Link-type nav items have no pageId
|
|
81
|
-
await navItemService.create({
|
|
82
|
-
type: "link",
|
|
83
|
-
label: "External",
|
|
84
|
-
url: "https://example.com",
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const pages = await pageService.listNotInNav();
|
|
88
|
-
expect(pages).toHaveLength(1);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("returns multiple pages correctly", async () => {
|
|
92
|
-
await pageService.create({ slug: "first", title: "First" });
|
|
93
|
-
await pageService.create({ slug: "second", title: "Second" });
|
|
94
|
-
await pageService.create({ slug: "third", title: "Third" });
|
|
95
|
-
|
|
96
|
-
// Add one to nav
|
|
97
|
-
const pages = await pageService.list();
|
|
98
|
-
await navItemService.create({
|
|
99
|
-
type: "page",
|
|
100
|
-
label: "Second",
|
|
101
|
-
url: "/second",
|
|
102
|
-
pageId: (
|
|
103
|
-
pages.find((p) => p.slug === "second") as (typeof pages)[number]
|
|
104
|
-
).id,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
const notInNav = await pageService.listNotInNav();
|
|
108
|
-
expect(notInNav).toHaveLength(2);
|
|
109
|
-
const slugs = notInNav.map((p) => p.slug);
|
|
110
|
-
expect(slugs).toContain("first");
|
|
111
|
-
expect(slugs).toContain("third");
|
|
112
|
-
expect(slugs).not.toContain("second");
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe("update nav item sync", () => {
|
|
117
|
-
it("syncs nav item label when page title changes", async () => {
|
|
118
|
-
const page = await pageService.create({
|
|
119
|
-
slug: "about",
|
|
120
|
-
title: "About",
|
|
121
|
-
});
|
|
122
|
-
await navItemService.create({
|
|
123
|
-
type: "page",
|
|
124
|
-
label: "About",
|
|
125
|
-
url: "/about",
|
|
126
|
-
pageId: page.id,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
await pageService.update(page.id, { title: "About Us" });
|
|
130
|
-
|
|
131
|
-
const navs = await navItemService.list();
|
|
132
|
-
expect(navs).toHaveLength(1);
|
|
133
|
-
expect(navs[0]?.label).toBe("About Us");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("syncs nav item url when page slug changes", async () => {
|
|
137
|
-
const page = await pageService.create({
|
|
138
|
-
slug: "about",
|
|
139
|
-
title: "About",
|
|
140
|
-
});
|
|
141
|
-
await navItemService.create({
|
|
142
|
-
type: "page",
|
|
143
|
-
label: "About",
|
|
144
|
-
url: "/about",
|
|
145
|
-
pageId: page.id,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
await pageService.update(page.id, { slug: "about-us" });
|
|
149
|
-
|
|
150
|
-
const navs = await navItemService.list();
|
|
151
|
-
expect(navs).toHaveLength(1);
|
|
152
|
-
expect(navs[0]?.url).toBe("/about-us");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("syncs both label and url when title and slug change together", async () => {
|
|
156
|
-
const page = await pageService.create({
|
|
157
|
-
slug: "about",
|
|
158
|
-
title: "About",
|
|
159
|
-
});
|
|
160
|
-
await navItemService.create({
|
|
161
|
-
type: "page",
|
|
162
|
-
label: "About",
|
|
163
|
-
url: "/about",
|
|
164
|
-
pageId: page.id,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
await pageService.update(page.id, {
|
|
168
|
-
title: "About Our Company",
|
|
169
|
-
slug: "about-our-company",
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const navs = await navItemService.list();
|
|
173
|
-
expect(navs).toHaveLength(1);
|
|
174
|
-
expect(navs[0]?.label).toBe("About Our Company");
|
|
175
|
-
expect(navs[0]?.url).toBe("/about-our-company");
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("does not change nav item label when title is unchanged", async () => {
|
|
179
|
-
const page = await pageService.create({
|
|
180
|
-
slug: "about",
|
|
181
|
-
title: "About",
|
|
182
|
-
});
|
|
183
|
-
await navItemService.create({
|
|
184
|
-
type: "page",
|
|
185
|
-
label: "Custom Label",
|
|
186
|
-
url: "/about",
|
|
187
|
-
pageId: page.id,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// Update body only, not title
|
|
191
|
-
await pageService.update(page.id, { body: "New content" });
|
|
192
|
-
|
|
193
|
-
const navs = await navItemService.list();
|
|
194
|
-
expect(navs[0]?.label).toBe("Custom Label");
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it("does not affect nav items for other pages", async () => {
|
|
198
|
-
const page1 = await pageService.create({
|
|
199
|
-
slug: "about",
|
|
200
|
-
title: "About",
|
|
201
|
-
});
|
|
202
|
-
const page2 = await pageService.create({
|
|
203
|
-
slug: "contact",
|
|
204
|
-
title: "Contact",
|
|
205
|
-
});
|
|
206
|
-
await navItemService.create({
|
|
207
|
-
type: "page",
|
|
208
|
-
label: "About",
|
|
209
|
-
url: "/about",
|
|
210
|
-
pageId: page1.id,
|
|
211
|
-
});
|
|
212
|
-
await navItemService.create({
|
|
213
|
-
type: "page",
|
|
214
|
-
label: "Contact",
|
|
215
|
-
url: "/contact",
|
|
216
|
-
pageId: page2.id,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
await pageService.update(page1.id, { title: "About Us" });
|
|
220
|
-
|
|
221
|
-
const navs = await navItemService.list();
|
|
222
|
-
const aboutNav = navs.find((n) => n.pageId === page1.id);
|
|
223
|
-
const contactNav = navs.find((n) => n.pageId === page2.id);
|
|
224
|
-
expect(aboutNav?.label).toBe("About Us");
|
|
225
|
-
expect(contactNav?.label).toBe("Contact");
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
describe("path registry integration", () => {
|
|
230
|
-
it("rejects reserved slug on create", async () => {
|
|
231
|
-
await expect(
|
|
232
|
-
pageService.create({ slug: "dash", title: "Dashboard" }),
|
|
233
|
-
).rejects.toThrow(ValidationError);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it("rejects slug that conflicts with a post path", async () => {
|
|
237
|
-
await postService.create({
|
|
238
|
-
format: "note",
|
|
239
|
-
body: "test",
|
|
240
|
-
path: "about",
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
await expect(
|
|
244
|
-
pageService.create({ slug: "about", title: "About" }),
|
|
245
|
-
).rejects.toThrow(ConflictError);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("rejects reserved slug on update", async () => {
|
|
249
|
-
const page = await pageService.create({
|
|
250
|
-
slug: "about",
|
|
251
|
-
title: "About",
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
await expect(
|
|
255
|
-
pageService.update(page.id, { slug: "api" }),
|
|
256
|
-
).rejects.toThrow(ValidationError);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("rejects slug update that conflicts with another entity", async () => {
|
|
260
|
-
const page = await pageService.create({
|
|
261
|
-
slug: "about",
|
|
262
|
-
title: "About",
|
|
263
|
-
});
|
|
264
|
-
await postService.create({
|
|
265
|
-
format: "note",
|
|
266
|
-
body: "test",
|
|
267
|
-
path: "contact",
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
await expect(
|
|
271
|
-
pageService.update(page.id, { slug: "contact" }),
|
|
272
|
-
).rejects.toThrow(ConflictError);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("releases path on delete", async () => {
|
|
276
|
-
const page = await pageService.create({
|
|
277
|
-
slug: "about",
|
|
278
|
-
title: "About",
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
await pageService.delete(page.id);
|
|
282
|
-
|
|
283
|
-
expect(await pathRegistry.isAvailable("about")).toBe(true);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it("releases old slug and claims new slug on update", async () => {
|
|
287
|
-
const page = await pageService.create({
|
|
288
|
-
slug: "about",
|
|
289
|
-
title: "About",
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
await pageService.update(page.id, { slug: "about-us" });
|
|
293
|
-
|
|
294
|
-
expect(await pathRegistry.isAvailable("about")).toBe(true);
|
|
295
|
-
expect(await pathRegistry.isAvailable("about-us")).toBe(false);
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
});
|