@jant/core 0.6.16 → 0.7.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 +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -13,14 +13,10 @@ import {
|
|
|
13
13
|
type DatabaseSchema,
|
|
14
14
|
} from "../db/schema-bundle.js";
|
|
15
15
|
import { createEntityId } from "../lib/ids.js";
|
|
16
|
-
import {
|
|
16
|
+
import { getCollectionPagePath } from "../lib/collection-paths.js";
|
|
17
|
+
import { NotFoundError, ValidationError } from "../lib/errors.js";
|
|
17
18
|
import { now } from "../lib/time.js";
|
|
18
|
-
import {
|
|
19
|
-
normalizePath,
|
|
20
|
-
normalizeSitePathPrefix,
|
|
21
|
-
stripSitePathPrefix,
|
|
22
|
-
toSameSitePath,
|
|
23
|
-
} from "../lib/url.js";
|
|
19
|
+
import { normalizePath, toInternalPath } from "../lib/url.js";
|
|
24
20
|
import {
|
|
25
21
|
COLLECTION_FRESHNESS_WINDOW_SECONDS,
|
|
26
22
|
DEFAULT_NAVIGATION_PROFILE,
|
|
@@ -50,13 +46,69 @@ export interface ListSuggestedLinksOptions {
|
|
|
50
46
|
|
|
51
47
|
// Re-export shared constraint detection — see db/dialect.ts
|
|
52
48
|
import { isUniqueConstraintError } from "../db/dialect.js";
|
|
49
|
+
import { createPathService, type PathService } from "./path.js";
|
|
50
|
+
|
|
51
|
+
export interface ListNavItemsOptions {
|
|
52
|
+
/**
|
|
53
|
+
* Content language whose view is being rendered, when it is not the site's
|
|
54
|
+
* primary one. Page items then resolve to the version of their target
|
|
55
|
+
* written in that language, so a reader browsing `/en` reaches the English
|
|
56
|
+
* About page. Items whose target has no version in that language are left
|
|
57
|
+
* pointing at the original — an asymmetric site is the normal case, and a
|
|
58
|
+
* link to the one About that exists beats no link at all.
|
|
59
|
+
*/
|
|
60
|
+
language?: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What an address the author pasted can become in navigation.
|
|
65
|
+
*
|
|
66
|
+
* An address that names something on this site should become an item that
|
|
67
|
+
* *follows* it — a page item tracks its post's title and slug, where a link
|
|
68
|
+
* item freezes both — so resolution is what turns a pasted URL into the right
|
|
69
|
+
* kind of item instead of a string that rots.
|
|
70
|
+
*/
|
|
71
|
+
export type NavTargetResolution =
|
|
72
|
+
/** A post navigation can point at, with the name it will show. */
|
|
73
|
+
| { status: "page"; page: NavigationPageCandidate }
|
|
74
|
+
| { status: "collection"; collection: NavTargetCollection }
|
|
75
|
+
/** A smart collection, which navigation follows exactly like a collection. */
|
|
76
|
+
| { status: "smart_collection"; smartCollection: NavTargetCollection }
|
|
77
|
+
/** A page on this site that navigation can only hold as a link. */
|
|
78
|
+
| { status: "link_only" }
|
|
79
|
+
/** Nothing to show in a menu until it has a title. */
|
|
80
|
+
| { status: "untitled" }
|
|
81
|
+
| { status: "unpublished" }
|
|
82
|
+
/** Published but private: a menu entry nobody could open. */
|
|
83
|
+
| { status: "private" }
|
|
84
|
+
| { status: "not_found" };
|
|
85
|
+
|
|
86
|
+
export interface NavTargetCollection {
|
|
87
|
+
id: string;
|
|
88
|
+
title: string;
|
|
89
|
+
slug: string;
|
|
90
|
+
}
|
|
53
91
|
|
|
54
92
|
export interface NavItemService {
|
|
55
|
-
list(): Promise<NavItem[]>;
|
|
93
|
+
list(options?: ListNavItemsOptions): Promise<NavItem[]>;
|
|
56
94
|
listPageCandidates(options?: {
|
|
57
95
|
query?: string;
|
|
58
96
|
limit?: number;
|
|
59
97
|
}): Promise<NavigationPageCandidate[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Resolve an address the author pasted into the navigation item it could
|
|
100
|
+
* become.
|
|
101
|
+
*
|
|
102
|
+
* The picker searches titles, which is no help when the author is holding
|
|
103
|
+
* the URL rather than the name. Every "no" is specific, because the page is
|
|
104
|
+
* one they can see in another tab: a draft, a private post, an untitled one.
|
|
105
|
+
*
|
|
106
|
+
* @param path - Internal path, as produced by `toInternalPath()`
|
|
107
|
+
* @returns The target to add, or why it cannot be added
|
|
108
|
+
* @example
|
|
109
|
+
* await navItems.resolveNavTarget("/about"); // { status: "page", page: … }
|
|
110
|
+
*/
|
|
111
|
+
resolveNavTarget(path: string): Promise<NavTargetResolution>;
|
|
60
112
|
getById(id: string): Promise<NavItem | null>;
|
|
61
113
|
create(data: CreateNavItem): Promise<NavItem>;
|
|
62
114
|
/**
|
|
@@ -82,20 +134,34 @@ export function createNavItemService(
|
|
|
82
134
|
db: Database,
|
|
83
135
|
siteId: string,
|
|
84
136
|
databaseSchema: DatabaseSchema = sqliteSchemaBundle,
|
|
137
|
+
paths?: PathService,
|
|
85
138
|
): NavItemService {
|
|
86
|
-
const {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
139
|
+
const {
|
|
140
|
+
navItems,
|
|
141
|
+
threadCollections,
|
|
142
|
+
posts,
|
|
143
|
+
pathRegistry,
|
|
144
|
+
collections,
|
|
145
|
+
smartCollections,
|
|
146
|
+
} = databaseSchema;
|
|
147
|
+
const resolvedPaths = paths ?? createPathService(db, siteId, databaseSchema);
|
|
148
|
+
|
|
149
|
+
function toNavItem(
|
|
150
|
+
row: typeof navItems.$inferSelect,
|
|
151
|
+
targetTitle?: string | null,
|
|
152
|
+
): NavItem {
|
|
153
|
+
const title = targetTitle?.trim();
|
|
90
154
|
return {
|
|
91
155
|
id: row.id,
|
|
92
156
|
siteId: row.siteId,
|
|
93
157
|
type: row.type as NavItemType,
|
|
94
158
|
systemKey: (row.systemKey as SystemNavKey | null) ?? undefined,
|
|
95
159
|
collectionId: row.collectionId ?? undefined,
|
|
160
|
+
smartCollectionId: row.smartCollectionId ?? undefined,
|
|
96
161
|
postId: row.postId ?? undefined,
|
|
97
162
|
label: row.label,
|
|
98
163
|
url: row.url,
|
|
164
|
+
...(title ? { targetTitle: title } : {}),
|
|
99
165
|
placement: (row.placement ?? "header") as NavItemPlacement,
|
|
100
166
|
position: row.position,
|
|
101
167
|
createdAt: row.createdAt,
|
|
@@ -103,6 +169,128 @@ export function createNavItemService(
|
|
|
103
169
|
};
|
|
104
170
|
}
|
|
105
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Read nav rows together with the current title of whatever each one points
|
|
174
|
+
* at.
|
|
175
|
+
*
|
|
176
|
+
* The title is joined rather than stored so renaming a page or a collection
|
|
177
|
+
* moves its nav entry with it. Two left joins on primary keys, on a table
|
|
178
|
+
* that holds a handful of rows — the alternative, a denormalized copy, costs
|
|
179
|
+
* a sync branch in every write path that can rename a target, including the
|
|
180
|
+
* ones that bypass the service entirely (import, GitHub sync, backfills).
|
|
181
|
+
*/
|
|
182
|
+
function selectNavItemsWithTargets() {
|
|
183
|
+
return db
|
|
184
|
+
.select({
|
|
185
|
+
nav: navItems,
|
|
186
|
+
postTitle: posts.title,
|
|
187
|
+
postTranslationGroupId: posts.translationGroupId,
|
|
188
|
+
collectionTitle: collections.title,
|
|
189
|
+
smartCollectionTitle: smartCollections.title,
|
|
190
|
+
})
|
|
191
|
+
.from(navItems)
|
|
192
|
+
.leftJoin(
|
|
193
|
+
posts,
|
|
194
|
+
and(eq(posts.siteId, siteId), eq(posts.id, navItems.postId)),
|
|
195
|
+
)
|
|
196
|
+
.leftJoin(
|
|
197
|
+
collections,
|
|
198
|
+
and(
|
|
199
|
+
eq(collections.siteId, siteId),
|
|
200
|
+
eq(collections.id, navItems.collectionId),
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
.leftJoin(
|
|
204
|
+
smartCollections,
|
|
205
|
+
and(
|
|
206
|
+
eq(smartCollections.siteId, siteId),
|
|
207
|
+
eq(smartCollections.id, navItems.smartCollectionId),
|
|
208
|
+
),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
type NavItemWithTargetRow = Awaited<
|
|
213
|
+
ReturnType<ReturnType<typeof selectNavItemsWithTargets>["where"]>
|
|
214
|
+
>[number];
|
|
215
|
+
|
|
216
|
+
function targetTitleOf(row: NavItemWithTargetRow): string | null {
|
|
217
|
+
if (row.nav.type === "page") return row.postTitle;
|
|
218
|
+
if (row.nav.type === "collection") return row.collectionTitle;
|
|
219
|
+
if (row.nav.type === "smart_collection") return row.smartCollectionTitle;
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Re-point page items at the version of their target written in `language`.
|
|
225
|
+
*
|
|
226
|
+
* A translated post is a different post at its own address, so following a
|
|
227
|
+
* nav entry into another language means swapping the destination, not
|
|
228
|
+
* prefixing the URL. Both the address and the title move together: a link
|
|
229
|
+
* that lands on Chinese content should be labelled in Chinese, whichever
|
|
230
|
+
* view the reader came from.
|
|
231
|
+
*
|
|
232
|
+
* Runs only in non-primary language views — the primary view's targets are
|
|
233
|
+
* already the ones stored — and only when a page item's target actually
|
|
234
|
+
* belongs to a translation group.
|
|
235
|
+
*/
|
|
236
|
+
async function resolvePageTranslations(
|
|
237
|
+
rows: NavItemWithTargetRow[],
|
|
238
|
+
language: string,
|
|
239
|
+
): Promise<Map<string, { url: string; title: string }>> {
|
|
240
|
+
const groupIds = [
|
|
241
|
+
...new Set(
|
|
242
|
+
rows.flatMap((row) =>
|
|
243
|
+
row.nav.type === "page" && row.postTranslationGroupId
|
|
244
|
+
? [row.postTranslationGroupId]
|
|
245
|
+
: [],
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
];
|
|
249
|
+
if (groupIds.length === 0) return new Map();
|
|
250
|
+
|
|
251
|
+
// Same bar the item had to clear to be added: a nav link must never lead
|
|
252
|
+
// to a draft, a private post, or an untitled one.
|
|
253
|
+
const siblings = await db
|
|
254
|
+
.select({
|
|
255
|
+
groupId: posts.translationGroupId,
|
|
256
|
+
title: posts.title,
|
|
257
|
+
path: pathRegistry.path,
|
|
258
|
+
})
|
|
259
|
+
.from(posts)
|
|
260
|
+
.innerJoin(
|
|
261
|
+
pathRegistry,
|
|
262
|
+
and(
|
|
263
|
+
eq(pathRegistry.siteId, siteId),
|
|
264
|
+
eq(pathRegistry.postId, posts.id),
|
|
265
|
+
eq(pathRegistry.kind, "slug"),
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
.where(
|
|
269
|
+
and(
|
|
270
|
+
eq(posts.siteId, siteId),
|
|
271
|
+
inArray(posts.translationGroupId, groupIds),
|
|
272
|
+
eq(posts.language, language),
|
|
273
|
+
eq(posts.status, "published"),
|
|
274
|
+
sql`${posts.visibility} != 'private'`,
|
|
275
|
+
isNull(posts.replyToId),
|
|
276
|
+
sql`trim(${posts.title}) != ''`,
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
return new Map(
|
|
281
|
+
siblings.flatMap((sibling) =>
|
|
282
|
+
sibling.groupId && sibling.title
|
|
283
|
+
? [
|
|
284
|
+
[
|
|
285
|
+
sibling.groupId,
|
|
286
|
+
{ url: withLeadingSlash(sibling.path), title: sibling.title },
|
|
287
|
+
] as const,
|
|
288
|
+
]
|
|
289
|
+
: [],
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
106
294
|
async function normalizeCreateData(data: CreateNavItem) {
|
|
107
295
|
if (data.type === "system") {
|
|
108
296
|
const config = SYSTEM_NAV_KEYS[data.systemKey];
|
|
@@ -114,22 +302,93 @@ export function createNavItemService(
|
|
|
114
302
|
type: data.type,
|
|
115
303
|
systemKey: data.systemKey,
|
|
116
304
|
collectionId: null,
|
|
305
|
+
smartCollectionId: null,
|
|
117
306
|
postId: null,
|
|
118
307
|
label: "",
|
|
119
308
|
url: config.url,
|
|
309
|
+
targetTitle: null,
|
|
120
310
|
placement: data.placement ?? config.defaultPlacement,
|
|
121
311
|
position: data.position,
|
|
122
312
|
};
|
|
123
313
|
}
|
|
124
314
|
|
|
125
315
|
if (data.type === "collection") {
|
|
316
|
+
// The slug lives in `path_registry`, the same place the page branch
|
|
317
|
+
// below reads it from — collections have no slug column of their own.
|
|
318
|
+
const rows = await db
|
|
319
|
+
.select({ title: collections.title, slug: pathRegistry.path })
|
|
320
|
+
.from(collections)
|
|
321
|
+
.innerJoin(
|
|
322
|
+
pathRegistry,
|
|
323
|
+
and(
|
|
324
|
+
eq(pathRegistry.siteId, siteId),
|
|
325
|
+
eq(pathRegistry.collectionId, collections.id),
|
|
326
|
+
eq(pathRegistry.kind, "slug"),
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
.where(
|
|
330
|
+
and(
|
|
331
|
+
eq(collections.siteId, siteId),
|
|
332
|
+
eq(collections.id, data.collectionId),
|
|
333
|
+
),
|
|
334
|
+
)
|
|
335
|
+
.limit(1);
|
|
336
|
+
const collection = rows[0];
|
|
337
|
+
if (!collection) {
|
|
338
|
+
throw new NotFoundError("Collection");
|
|
339
|
+
}
|
|
340
|
+
|
|
126
341
|
return {
|
|
127
342
|
type: data.type,
|
|
128
343
|
systemKey: null,
|
|
129
344
|
collectionId: data.collectionId,
|
|
345
|
+
smartCollectionId: null,
|
|
346
|
+
postId: null,
|
|
347
|
+
// Stored empty unless the author typed something: the item then shows
|
|
348
|
+
// the collection's current title, and follows it when renamed.
|
|
349
|
+
label: (data.label?.trim() ?? "").slice(0, 100),
|
|
350
|
+
url: getCollectionPagePath(collection.slug),
|
|
351
|
+
targetTitle: collection.title,
|
|
352
|
+
placement: data.placement ?? "header",
|
|
353
|
+
position: data.position,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (data.type === "smart_collection") {
|
|
358
|
+
// Same shape as the collection branch: the slug lives in `path_registry`,
|
|
359
|
+
// and an omitted label means the item follows the title.
|
|
360
|
+
const rows = await db
|
|
361
|
+
.select({ title: smartCollections.title, slug: pathRegistry.path })
|
|
362
|
+
.from(smartCollections)
|
|
363
|
+
.innerJoin(
|
|
364
|
+
pathRegistry,
|
|
365
|
+
and(
|
|
366
|
+
eq(pathRegistry.siteId, siteId),
|
|
367
|
+
eq(pathRegistry.smartCollectionId, smartCollections.id),
|
|
368
|
+
eq(pathRegistry.kind, "slug"),
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
.where(
|
|
372
|
+
and(
|
|
373
|
+
eq(smartCollections.siteId, siteId),
|
|
374
|
+
eq(smartCollections.id, data.smartCollectionId),
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
.limit(1);
|
|
378
|
+
const smartCollection = rows[0];
|
|
379
|
+
if (!smartCollection) {
|
|
380
|
+
throw new NotFoundError("Smart collection");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
type: data.type,
|
|
385
|
+
systemKey: null,
|
|
386
|
+
collectionId: null,
|
|
387
|
+
smartCollectionId: data.smartCollectionId,
|
|
130
388
|
postId: null,
|
|
131
|
-
label: data.label,
|
|
132
|
-
url:
|
|
389
|
+
label: (data.label?.trim() ?? "").slice(0, 100),
|
|
390
|
+
url: getCollectionPagePath(smartCollection.slug),
|
|
391
|
+
targetTitle: smartCollection.title,
|
|
133
392
|
placement: data.placement ?? "header",
|
|
134
393
|
position: data.position,
|
|
135
394
|
};
|
|
@@ -173,9 +432,13 @@ export function createNavItemService(
|
|
|
173
432
|
type: data.type,
|
|
174
433
|
systemKey: null,
|
|
175
434
|
collectionId: null,
|
|
435
|
+
smartCollectionId: null,
|
|
176
436
|
postId: data.postId,
|
|
177
|
-
|
|
437
|
+
// Stored empty unless the author typed something: the item then shows
|
|
438
|
+
// the page's current title, and follows it when renamed.
|
|
439
|
+
label: (data.label?.trim() ?? "").slice(0, 100),
|
|
178
440
|
url: withLeadingSlash(page.slug),
|
|
441
|
+
targetTitle: page.title,
|
|
179
442
|
placement: data.placement ?? "header",
|
|
180
443
|
position: data.position,
|
|
181
444
|
};
|
|
@@ -185,14 +448,25 @@ export function createNavItemService(
|
|
|
185
448
|
type: data.type,
|
|
186
449
|
systemKey: null,
|
|
187
450
|
collectionId: null,
|
|
451
|
+
smartCollectionId: null,
|
|
188
452
|
postId: null,
|
|
189
453
|
label: data.label,
|
|
190
454
|
url: data.url,
|
|
455
|
+
targetTitle: null,
|
|
191
456
|
placement: data.placement ?? "header",
|
|
192
457
|
position: data.position,
|
|
193
458
|
};
|
|
194
459
|
}
|
|
195
460
|
|
|
461
|
+
/** Read one nav item with its target's current title. */
|
|
462
|
+
async function readNavItem(id: string): Promise<NavItem | null> {
|
|
463
|
+
const result = await selectNavItemsWithTargets()
|
|
464
|
+
.where(and(eq(navItems.siteId, siteId), eq(navItems.id, id)))
|
|
465
|
+
.limit(1);
|
|
466
|
+
const row = result[0];
|
|
467
|
+
return row ? toNavItem(row.nav, targetTitleOf(row)) : null;
|
|
468
|
+
}
|
|
469
|
+
|
|
196
470
|
function withLeadingSlash(path: string): string {
|
|
197
471
|
const normalized = normalizePath(path);
|
|
198
472
|
return normalized ? `/${normalized}` : "/";
|
|
@@ -202,39 +476,11 @@ export function createNavItemService(
|
|
|
202
476
|
url: string,
|
|
203
477
|
options: ListSuggestedLinksOptions = {},
|
|
204
478
|
): string | null {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
options.sitePathPrefix ?? "",
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
if (sameSitePath !== null) {
|
|
211
|
-
try {
|
|
212
|
-
const pathname = new URL(sameSitePath, "https://jant.invalid").pathname;
|
|
213
|
-
const internalPath = stripSitePathPrefix(pathname, sitePathPrefix);
|
|
214
|
-
return internalPath ? withLeadingSlash(internalPath) : null;
|
|
215
|
-
} catch {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
value.startsWith("http://") ||
|
|
222
|
-
value.startsWith("https://") ||
|
|
223
|
-
value.startsWith("//") ||
|
|
224
|
-
value.startsWith("mailto:") ||
|
|
225
|
-
value.startsWith("tel:") ||
|
|
226
|
-
value.startsWith("#")
|
|
227
|
-
) {
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
const pathname = new URL(value, "https://jant.invalid").pathname;
|
|
233
|
-
const internalPath = stripSitePathPrefix(pathname, sitePathPrefix);
|
|
234
|
-
return internalPath ? withLeadingSlash(internalPath) : null;
|
|
235
|
-
} catch {
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
479
|
+
const internalPath = toInternalPath(url, {
|
|
480
|
+
siteOrigins: options.siteOrigin ? [options.siteOrigin] : [],
|
|
481
|
+
sitePathPrefix: options.sitePathPrefix ?? "",
|
|
482
|
+
});
|
|
483
|
+
return internalPath ? withLeadingSlash(internalPath) : null;
|
|
238
484
|
}
|
|
239
485
|
|
|
240
486
|
function normalizeSuggestedLabel(
|
|
@@ -299,13 +545,26 @@ export function createNavItemService(
|
|
|
299
545
|
}
|
|
300
546
|
|
|
301
547
|
return {
|
|
302
|
-
async list() {
|
|
303
|
-
const rows = await
|
|
304
|
-
.select()
|
|
305
|
-
.from(navItems)
|
|
548
|
+
async list(options = {}) {
|
|
549
|
+
const rows = await selectNavItemsWithTargets()
|
|
306
550
|
.where(eq(navItems.siteId, siteId))
|
|
307
551
|
.orderBy(asc(navItems.position));
|
|
308
|
-
|
|
552
|
+
|
|
553
|
+
const translations = options.language
|
|
554
|
+
? await resolvePageTranslations(rows, options.language)
|
|
555
|
+
: null;
|
|
556
|
+
|
|
557
|
+
return rows.map((row) => {
|
|
558
|
+
const translated =
|
|
559
|
+
translations && row.nav.type === "page" && row.postTranslationGroupId
|
|
560
|
+
? translations.get(row.postTranslationGroupId)
|
|
561
|
+
: undefined;
|
|
562
|
+
if (!translated) return toNavItem(row.nav, targetTitleOf(row));
|
|
563
|
+
// `postId` keeps naming the item's configured target: it is what the
|
|
564
|
+
// settings UI edits and what "already in navigation" checks against.
|
|
565
|
+
// Only where the reader lands, and what it is called, follow the view.
|
|
566
|
+
return toNavItem({ ...row.nav, url: translated.url }, translated.title);
|
|
567
|
+
});
|
|
309
568
|
},
|
|
310
569
|
|
|
311
570
|
async listPageCandidates(options = {}) {
|
|
@@ -365,13 +624,93 @@ export function createNavItemService(
|
|
|
365
624
|
);
|
|
366
625
|
},
|
|
367
626
|
|
|
368
|
-
async
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
627
|
+
async resolveNavTarget(path) {
|
|
628
|
+
const target = await resolvedPaths.resolveTarget(path);
|
|
629
|
+
if (!target) return { status: "not_found" };
|
|
630
|
+
|
|
631
|
+
if (target.targetType === "collection" && target.collectionId) {
|
|
632
|
+
const rows = await db
|
|
633
|
+
.select({ id: collections.id, title: collections.title })
|
|
634
|
+
.from(collections)
|
|
635
|
+
.where(
|
|
636
|
+
and(
|
|
637
|
+
eq(collections.siteId, siteId),
|
|
638
|
+
eq(collections.id, target.collectionId),
|
|
639
|
+
),
|
|
640
|
+
)
|
|
641
|
+
.limit(1);
|
|
642
|
+
const row = rows[0];
|
|
643
|
+
if (!row) return { status: "not_found" };
|
|
644
|
+
|
|
645
|
+
const slug = await resolvedPaths.getCollectionSlug(row.id);
|
|
646
|
+
return {
|
|
647
|
+
status: "collection",
|
|
648
|
+
collection: { id: row.id, title: row.title, slug: slug ?? "" },
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (
|
|
653
|
+
target.targetType === "smart_collection" &&
|
|
654
|
+
target.smartCollectionId
|
|
655
|
+
) {
|
|
656
|
+
const rows = await db
|
|
657
|
+
.select({ id: smartCollections.id, title: smartCollections.title })
|
|
658
|
+
.from(smartCollections)
|
|
659
|
+
.where(
|
|
660
|
+
and(
|
|
661
|
+
eq(smartCollections.siteId, siteId),
|
|
662
|
+
eq(smartCollections.id, target.smartCollectionId),
|
|
663
|
+
),
|
|
664
|
+
)
|
|
665
|
+
.limit(1);
|
|
666
|
+
const row = rows[0];
|
|
667
|
+
if (!row) return { status: "not_found" };
|
|
668
|
+
|
|
669
|
+
const slug = await resolvedPaths.getSmartCollectionSlug(row.id);
|
|
670
|
+
return {
|
|
671
|
+
status: "smart_collection",
|
|
672
|
+
smartCollection: { id: row.id, title: row.title, slug: slug ?? "" },
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (target.targetType !== "post" || !target.postId) {
|
|
677
|
+
// A real address — an archive URL, say — that no navigation item can
|
|
678
|
+
// track. It can still be held as a plain link.
|
|
679
|
+
return { status: "link_only" };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const rows = await db
|
|
683
|
+
.select({
|
|
684
|
+
id: posts.id,
|
|
685
|
+
title: posts.title,
|
|
686
|
+
status: posts.status,
|
|
687
|
+
visibility: posts.visibility,
|
|
688
|
+
updatedAt: posts.updatedAt,
|
|
689
|
+
})
|
|
690
|
+
.from(posts)
|
|
691
|
+
.where(and(eq(posts.siteId, siteId), eq(posts.id, target.postId)))
|
|
373
692
|
.limit(1);
|
|
374
|
-
|
|
693
|
+
const row = rows[0];
|
|
694
|
+
if (!row) return { status: "not_found" };
|
|
695
|
+
|
|
696
|
+
if (row.status !== "published") return { status: "unpublished" };
|
|
697
|
+
if (row.visibility === "private") return { status: "private" };
|
|
698
|
+
const title = row.title?.trim() ?? "";
|
|
699
|
+
if (!title) return { status: "untitled" };
|
|
700
|
+
|
|
701
|
+
// The canonical slug rather than the address that was pasted: an alias
|
|
702
|
+
// resolves here too, and the item should show where the page lives.
|
|
703
|
+
const slug = await resolvedPaths.getPostSlug(row.id);
|
|
704
|
+
if (!slug) return { status: "not_found" };
|
|
705
|
+
|
|
706
|
+
return {
|
|
707
|
+
status: "page",
|
|
708
|
+
page: { id: row.id, title, slug, updatedAt: row.updatedAt },
|
|
709
|
+
};
|
|
710
|
+
},
|
|
711
|
+
|
|
712
|
+
async getById(id) {
|
|
713
|
+
return readNavItem(id);
|
|
375
714
|
},
|
|
376
715
|
|
|
377
716
|
async create(data) {
|
|
@@ -413,6 +752,25 @@ export function createNavItemService(
|
|
|
413
752
|
}
|
|
414
753
|
}
|
|
415
754
|
|
|
755
|
+
if (normalized.smartCollectionId) {
|
|
756
|
+
const existingSmartCollectionItem = await db
|
|
757
|
+
.select({ id: navItems.id })
|
|
758
|
+
.from(navItems)
|
|
759
|
+
.where(
|
|
760
|
+
and(
|
|
761
|
+
eq(navItems.siteId, siteId),
|
|
762
|
+
eq(navItems.smartCollectionId, normalized.smartCollectionId),
|
|
763
|
+
),
|
|
764
|
+
)
|
|
765
|
+
.limit(1);
|
|
766
|
+
|
|
767
|
+
if (existingSmartCollectionItem[0]) {
|
|
768
|
+
throw new ValidationError(
|
|
769
|
+
"Smart collection already added to navigation",
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
416
774
|
if (normalized.postId) {
|
|
417
775
|
const existingPageItem = await db
|
|
418
776
|
.select({ id: navItems.id })
|
|
@@ -439,6 +797,7 @@ export function createNavItemService(
|
|
|
439
797
|
type: normalized.type,
|
|
440
798
|
systemKey: normalized.systemKey,
|
|
441
799
|
collectionId: normalized.collectionId,
|
|
800
|
+
smartCollectionId: normalized.smartCollectionId,
|
|
442
801
|
postId: normalized.postId,
|
|
443
802
|
label: normalized.label,
|
|
444
803
|
url: normalized.url,
|
|
@@ -450,7 +809,7 @@ export function createNavItemService(
|
|
|
450
809
|
.returning();
|
|
451
810
|
|
|
452
811
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
453
|
-
return toNavItem(result[0]
|
|
812
|
+
return toNavItem(result[0]!, normalized.targetTitle);
|
|
454
813
|
}
|
|
455
814
|
|
|
456
815
|
for (let attempt = 0; attempt < POSITION_RETRY_ATTEMPTS; attempt += 1) {
|
|
@@ -463,6 +822,7 @@ export function createNavItemService(
|
|
|
463
822
|
type: normalized.type,
|
|
464
823
|
systemKey: normalized.systemKey,
|
|
465
824
|
collectionId: normalized.collectionId,
|
|
825
|
+
smartCollectionId: normalized.smartCollectionId,
|
|
466
826
|
postId: normalized.postId,
|
|
467
827
|
label: normalized.label,
|
|
468
828
|
url: normalized.url,
|
|
@@ -474,7 +834,7 @@ export function createNavItemService(
|
|
|
474
834
|
.returning();
|
|
475
835
|
|
|
476
836
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- DB insert with .returning() always returns inserted row
|
|
477
|
-
return toNavItem(result[0]
|
|
837
|
+
return toNavItem(result[0]!, normalized.targetTitle);
|
|
478
838
|
} catch (err) {
|
|
479
839
|
if (
|
|
480
840
|
!isUniqueConstraintError(err) ||
|
|
@@ -552,11 +912,13 @@ export function createNavItemService(
|
|
|
552
912
|
}
|
|
553
913
|
}
|
|
554
914
|
|
|
555
|
-
//
|
|
915
|
+
// Clearing the label is how an item goes back to following what it
|
|
916
|
+
// points at, so only free-form links — which have no target to follow —
|
|
917
|
+
// still require one.
|
|
556
918
|
if (
|
|
557
919
|
data.label !== undefined &&
|
|
558
|
-
!data.label &&
|
|
559
|
-
existing[0].type
|
|
920
|
+
!data.label.trim() &&
|
|
921
|
+
existing[0].type === "link"
|
|
560
922
|
) {
|
|
561
923
|
throw new ValidationError("Label is required");
|
|
562
924
|
}
|
|
@@ -565,7 +927,7 @@ export function createNavItemService(
|
|
|
565
927
|
const result = await db
|
|
566
928
|
.update(navItems)
|
|
567
929
|
.set({
|
|
568
|
-
...(data.label !== undefined && { label: data.label }),
|
|
930
|
+
...(data.label !== undefined && { label: data.label.trim() }),
|
|
569
931
|
...(data.url !== undefined && { url: data.url }),
|
|
570
932
|
...(data.placement !== undefined && { placement: data.placement }),
|
|
571
933
|
...(data.position !== undefined && { position: data.position }),
|
|
@@ -574,7 +936,7 @@ export function createNavItemService(
|
|
|
574
936
|
.where(and(eq(navItems.siteId, siteId), eq(navItems.id, id)))
|
|
575
937
|
.returning();
|
|
576
938
|
|
|
577
|
-
return result[0] ?
|
|
939
|
+
return result[0] ? readNavItem(id) : null;
|
|
578
940
|
},
|
|
579
941
|
|
|
580
942
|
async delete(id) {
|
|
@@ -606,7 +968,7 @@ export function createNavItemService(
|
|
|
606
968
|
.where(and(eq(navItems.siteId, siteId), eq(navItems.id, id)))
|
|
607
969
|
.returning();
|
|
608
970
|
|
|
609
|
-
return result[0] ?
|
|
971
|
+
return result[0] ? readNavItem(id) : null;
|
|
610
972
|
} catch (err) {
|
|
611
973
|
if (
|
|
612
974
|
!isUniqueConstraintError(err) ||
|