@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
|
@@ -38,9 +38,9 @@ describe("Settings API Routes", () => {
|
|
|
38
38
|
expect(body.settings.SITE_NAME).toBe("Jant");
|
|
39
39
|
expect(body.settings.SITE_DESCRIPTION).toBe("");
|
|
40
40
|
expect(body.settings.SITE_LANGUAGE).toBe("en");
|
|
41
|
-
expect(body.settings.PAGE_SIZE).toBe("
|
|
42
|
-
expect(body.settings.SEARCH_PAGE_SIZE).toBe("
|
|
43
|
-
expect(body.settings.ARCHIVE_PAGE_SIZE).toBe("
|
|
41
|
+
expect(body.settings.PAGE_SIZE).toBe("25");
|
|
42
|
+
expect(body.settings.SEARCH_PAGE_SIZE).toBe("25");
|
|
43
|
+
expect(body.settings.ARCHIVE_PAGE_SIZE).toBe("25");
|
|
44
44
|
expect(body.settings.RSS_PUBLISH_DELAY_SECONDS).toBe("0");
|
|
45
45
|
});
|
|
46
46
|
|
|
@@ -199,6 +199,27 @@ describe("Settings API Routes", () => {
|
|
|
199
199
|
expect(body.details.rejectedKeys).toContain("AUTH_SECRET");
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
+
it("rejects the multilingual keys, which only the language page may write", async () => {
|
|
203
|
+
const { app } = createTestApp({ authenticated: true });
|
|
204
|
+
app.route("/api/settings", settingsApiRoutes);
|
|
205
|
+
|
|
206
|
+
const res = await app.request("/api/settings", {
|
|
207
|
+
method: "PUT",
|
|
208
|
+
headers: { "Content-Type": "application/json" },
|
|
209
|
+
body: JSON.stringify({
|
|
210
|
+
MULTILINGUAL_ENABLED: "true",
|
|
211
|
+
ADDITIONAL_LANGUAGES: "en",
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Writing these directly would skip the confirmation that stamps
|
|
216
|
+
// existing posts and the URL-prefix conflict check.
|
|
217
|
+
expect(res.status).toBe(400);
|
|
218
|
+
const body = await res.json();
|
|
219
|
+
expect(body.details.rejectedKeys).toContain("MULTILINGUAL_ENABLED");
|
|
220
|
+
expect(body.details.rejectedKeys).toContain("ADDITIONAL_LANGUAGES");
|
|
221
|
+
});
|
|
222
|
+
|
|
202
223
|
it("rejects internal keys", async () => {
|
|
203
224
|
const { app } = createTestApp({ authenticated: true });
|
|
204
225
|
app.route("/api/settings", settingsApiRoutes);
|
|
@@ -3,6 +3,7 @@ import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
|
3
3
|
import { DEFAULT_TEST_SITE_ID } from "../../../__tests__/helpers/db.js";
|
|
4
4
|
import { telegramWebhookRoutes } from "../telegram.js";
|
|
5
5
|
import type { StorageDriver } from "../../../lib/storage.js";
|
|
6
|
+
import type { DeferredPendingEnv } from "../../../lib/deferred.js";
|
|
6
7
|
|
|
7
8
|
const BOT_ID = "111111";
|
|
8
9
|
const BOT_TOKEN = `${BOT_ID}:AA-test-token`;
|
|
@@ -117,13 +118,13 @@ function setup(
|
|
|
117
118
|
|
|
118
119
|
// The webhook ACKs Telegram immediately and runs the album flush in
|
|
119
120
|
// detached promises so subsequent webhooks on the same chat aren't
|
|
120
|
-
// serialized behind it. Tests have no `executionCtx.waitUntil`, so
|
|
121
|
-
//
|
|
122
|
-
// — collect them here so tests can await the flush
|
|
121
|
+
// serialized behind it. Tests have no `executionCtx.waitUntil`, so
|
|
122
|
+
// `runDeferred` falls back to writing each promise into
|
|
123
|
+
// `env.__pendingDeferred` — collect them here so tests can await the flush
|
|
124
|
+
// before asserting.
|
|
123
125
|
const pending: Promise<unknown>[] = [];
|
|
124
126
|
ctx.app.use("/api/telegram/*", async (c, next) => {
|
|
125
|
-
(c.env as
|
|
126
|
-
pending;
|
|
127
|
+
(c.env as DeferredPendingEnv).__pendingDeferred = pending;
|
|
127
128
|
return next();
|
|
128
129
|
});
|
|
129
130
|
ctx.app.route("/api/telegram", telegramWebhookRoutes);
|
|
@@ -9,9 +9,11 @@ import type { AppVariables } from "../../types/app-context.js";
|
|
|
9
9
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
10
10
|
import { requirePublicApiAccess } from "../../middleware/public-content-access.js";
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
CollectionDirectoryRowIdSchema,
|
|
13
13
|
CollectionDescriptionValueSchema,
|
|
14
|
+
CollectionIdSchema,
|
|
14
15
|
CollectionSortOrderSchema,
|
|
16
|
+
ContentLanguageSchema,
|
|
15
17
|
CreateCollectionDirectoryItemSchema,
|
|
16
18
|
CreateCollectionSchema,
|
|
17
19
|
PostIdSchema,
|
|
@@ -36,12 +38,24 @@ const ThreadAssignSchema = z.object({
|
|
|
36
38
|
});
|
|
37
39
|
|
|
38
40
|
const MoveSchema = z.object({
|
|
39
|
-
after:
|
|
40
|
-
before:
|
|
41
|
+
after: CollectionDirectoryRowIdSchema.nullable().optional(),
|
|
42
|
+
before: CollectionDirectoryRowIdSchema.nullable().optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const SlugQuerySchema = z.object({
|
|
46
|
+
mode: z.literal("check"),
|
|
47
|
+
slug: z.string().trim().toLowerCase().min(1).max(200),
|
|
48
|
+
collectionId: CollectionIdSchema.optional(),
|
|
41
49
|
});
|
|
42
50
|
|
|
43
51
|
const ListCollectionsQuerySchema = z.object({
|
|
44
52
|
view: z.enum(["compose"]).optional(),
|
|
53
|
+
/**
|
|
54
|
+
* Content language of the calling view. The directory's Thread counts are
|
|
55
|
+
* narrowed by it, so the page and this endpoint have to be asked the same
|
|
56
|
+
* question — see `jant-collection-directory`.
|
|
57
|
+
*/
|
|
58
|
+
lang: ContentLanguageSchema.optional(),
|
|
45
59
|
});
|
|
46
60
|
|
|
47
61
|
// List collections (includes Thread counts and directory items)
|
|
@@ -50,6 +64,11 @@ collectionsApiRoutes.get("/", requirePublicApiAccess(), async (c) => {
|
|
|
50
64
|
|
|
51
65
|
if (query.view === "compose") {
|
|
52
66
|
c.header("Cache-Control", "no-store");
|
|
67
|
+
// Smart collections are deliberately absent. A post cannot be added to one
|
|
68
|
+
// by hand — its conditions decide, permanently — so offering it here would
|
|
69
|
+
// be a control that does nothing. Every "add this post to a collection"
|
|
70
|
+
// surface leaves them out for the same reason; see the smart collections
|
|
71
|
+
// design notes on the asymmetry, which is not a gap to be filled in later.
|
|
53
72
|
const collections = await c.var.services.collections.listByRecentActivity();
|
|
54
73
|
return c.json({
|
|
55
74
|
collections,
|
|
@@ -57,10 +76,14 @@ collectionsApiRoutes.get("/", requirePublicApiAccess(), async (c) => {
|
|
|
57
76
|
});
|
|
58
77
|
}
|
|
59
78
|
|
|
60
|
-
const directoryData = await c.var.services.collections.listDirectoryData(
|
|
79
|
+
const directoryData = await c.var.services.collections.listDirectoryData({
|
|
80
|
+
isAuthenticated: c.var.isAuthenticated,
|
|
81
|
+
lang: query.lang,
|
|
82
|
+
});
|
|
61
83
|
|
|
62
84
|
return c.json({
|
|
63
85
|
collections: directoryData.collections,
|
|
86
|
+
smartCollections: directoryData.smartCollections,
|
|
64
87
|
directoryItems: directoryData.directoryItems,
|
|
65
88
|
});
|
|
66
89
|
});
|
|
@@ -98,13 +121,17 @@ collectionsApiRoutes.put(
|
|
|
98
121
|
);
|
|
99
122
|
|
|
100
123
|
// Move directory item — must be before /:id
|
|
124
|
+
//
|
|
125
|
+
// The moved row and its neighbours may each be named by a collection or smart
|
|
126
|
+
// collection id: those are the rows the directory renders without one of its
|
|
127
|
+
// own. The service places them before it moves anything.
|
|
101
128
|
collectionsApiRoutes.put(
|
|
102
129
|
"/directory-items/:id/move",
|
|
103
130
|
requireAuthApi(),
|
|
104
131
|
async (c) => {
|
|
105
|
-
const id =
|
|
132
|
+
const id = parseValidated(
|
|
133
|
+
CollectionDirectoryRowIdSchema,
|
|
106
134
|
c.req.param("id"),
|
|
107
|
-
ID_PREFIX.collectionDirectoryItem,
|
|
108
135
|
);
|
|
109
136
|
const body = parseValidated(MoveSchema, await c.req.json());
|
|
110
137
|
|
|
@@ -135,6 +162,22 @@ collectionsApiRoutes.delete(
|
|
|
135
162
|
},
|
|
136
163
|
);
|
|
137
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Address availability, in the shape smart collections already use.
|
|
167
|
+
*
|
|
168
|
+
* Declared before `/:id` so the literal path wins. The editing dialog asks
|
|
169
|
+
* while the author types, so a clash is answered where the decision is being
|
|
170
|
+
* made rather than on write.
|
|
171
|
+
*/
|
|
172
|
+
collectionsApiRoutes.get("/slug", requireAuthApi(), async (c) => {
|
|
173
|
+
const query = parseValidated(SlugQuerySchema, c.req.query());
|
|
174
|
+
const available = await c.var.services.collections.checkSlugAvailability(
|
|
175
|
+
query.slug,
|
|
176
|
+
query.collectionId,
|
|
177
|
+
);
|
|
178
|
+
return c.json({ slug: query.slug, available });
|
|
179
|
+
});
|
|
180
|
+
|
|
138
181
|
// Get single collection
|
|
139
182
|
collectionsApiRoutes.get("/:id", requirePublicApiAccess(), async (c) => {
|
|
140
183
|
const id = parseIdParam(c.req.param("id"), ID_PREFIX.collection);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discover API Routes
|
|
3
|
+
*
|
|
4
|
+
* The one address a Discover directory calls besides the feeds it polls.
|
|
5
|
+
* `GET /api/discover/posts` says, for each post ID it names, whether the post
|
|
6
|
+
* is in this site's Latest feed and whether its Thread is in the featured
|
|
7
|
+
* feed. The feed cannot say this about a post pushed past its length, and the
|
|
8
|
+
* permalink cannot say it about a post hidden from Latest or unfeatured, which
|
|
9
|
+
* is still a live page — so the directory asks instead of guessing.
|
|
10
|
+
*
|
|
11
|
+
* Specified in `docs/feeds.md`. A directory never builds the address: every
|
|
12
|
+
* feed names it in `<jant:discover status="…">`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Hono } from "hono";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import type { Bindings } from "../../types.js";
|
|
18
|
+
import type { AppVariables } from "../../types/app-context.js";
|
|
19
|
+
import { DISCOVER_STATUS_MAX_IDS } from "../../lib/discover.js";
|
|
20
|
+
import {
|
|
21
|
+
featuredFeedSelection,
|
|
22
|
+
getRssPublishedBefore,
|
|
23
|
+
latestFeedSelection,
|
|
24
|
+
} from "../../lib/feed-policy.js";
|
|
25
|
+
import { createTypeIdSchema, ID_PREFIX } from "../../lib/ids.js";
|
|
26
|
+
import { ContentLanguageSchema, parseValidated } from "../../lib/schemas.js";
|
|
27
|
+
|
|
28
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
29
|
+
|
|
30
|
+
export const discoverApiRoutes = new Hono<Env>();
|
|
31
|
+
|
|
32
|
+
const PostStatusQuerySchema = z.object({
|
|
33
|
+
id: z
|
|
34
|
+
.array(createTypeIdSchema(ID_PREFIX.post))
|
|
35
|
+
.min(1, "Name at least one post with id=")
|
|
36
|
+
.max(
|
|
37
|
+
DISCOVER_STATUS_MAX_IDS,
|
|
38
|
+
`Ask about at most ${DISCOVER_STATUS_MAX_IDS} posts at a time`,
|
|
39
|
+
),
|
|
40
|
+
/** The language view the answer is about, as a language feed is scoped. */
|
|
41
|
+
lang: ContentLanguageSchema.optional(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** Where one post stands in the feeds a directory reads. */
|
|
45
|
+
export interface DiscoverPostStatus {
|
|
46
|
+
id: string;
|
|
47
|
+
/** In the Latest feed. */
|
|
48
|
+
latest: boolean;
|
|
49
|
+
/** Its Thread is in the featured feed. */
|
|
50
|
+
featured: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
discoverApiRoutes.get("/posts", async (c) => {
|
|
54
|
+
// It answers for a site that takes part, and follows the Discover setting
|
|
55
|
+
// rather than `PUBLIC_API_ENABLED`: it says nothing the public feeds do not,
|
|
56
|
+
// and a site that turned its JSON API off has not left Discover.
|
|
57
|
+
if (c.var.appConfig.discover === "none") return c.notFound();
|
|
58
|
+
|
|
59
|
+
const query = parseValidated(PostStatusQuerySchema, {
|
|
60
|
+
id: c.req.queries("id") ?? [],
|
|
61
|
+
lang: c.req.query("lang"),
|
|
62
|
+
});
|
|
63
|
+
const ids = [...new Set(query.id)];
|
|
64
|
+
// The feeds hold a new post back for the RSS delay, and so does this: the
|
|
65
|
+
// answer is about the feeds as a reader would fetch them now.
|
|
66
|
+
const publishedBefore = getRssPublishedBefore(
|
|
67
|
+
c.var.appConfig.rssPublishDelaySeconds,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const [latest, featured] = await Promise.all([
|
|
71
|
+
c.var.services.posts.list({
|
|
72
|
+
...latestFeedSelection({ lang: query.lang, publishedBefore }),
|
|
73
|
+
ids,
|
|
74
|
+
limit: ids.length,
|
|
75
|
+
}),
|
|
76
|
+
c.var.services.posts.listFeaturedThreadRootIds({
|
|
77
|
+
...featuredFeedSelection({ lang: query.lang, publishedBefore }),
|
|
78
|
+
threadIds: ids,
|
|
79
|
+
limit: ids.length,
|
|
80
|
+
}),
|
|
81
|
+
]);
|
|
82
|
+
const inLatest = new Set(latest.map((post) => post.id));
|
|
83
|
+
const inFeatured = new Set(featured);
|
|
84
|
+
|
|
85
|
+
// Every ID asked about is answered, in the order asked. A post that is
|
|
86
|
+
// deleted, private, a draft, a reply, or not this site's at all comes back
|
|
87
|
+
// `false` on both: to a directory they all mean the same thing.
|
|
88
|
+
return c.json({
|
|
89
|
+
posts: ids.map((id): DiscoverPostStatus => ({
|
|
90
|
+
id,
|
|
91
|
+
latest: inLatest.has(id),
|
|
92
|
+
featured: inFeatured.has(id),
|
|
93
|
+
})),
|
|
94
|
+
});
|
|
95
|
+
});
|
package/src/routes/api/export.ts
CHANGED
|
@@ -23,10 +23,13 @@ exportApiRoutes.post("/hugo", requireAuthApi(), async (c) => {
|
|
|
23
23
|
siteUrl: appConfig.siteUrl,
|
|
24
24
|
siteDescription: appConfig.siteDescription,
|
|
25
25
|
siteLanguage: appConfig.siteLanguage,
|
|
26
|
+
multilingualEnabled: appConfig.multilingualEnabled,
|
|
27
|
+
additionalLanguages: appConfig.additionalLanguages,
|
|
26
28
|
showJantBrandingOnHome: appConfig.showJantBrandingOnHome,
|
|
27
29
|
publicApiEnabled: appConfig.publicApiEnabled,
|
|
28
30
|
rssFeedsEnabled: appConfig.rssFeedsEnabled,
|
|
29
31
|
mainRssFeed: appConfig.mainRssFeed,
|
|
32
|
+
archiveDefaultLayout: appConfig.archiveDefaultLayout,
|
|
30
33
|
siteFooter: appConfig.siteFooter,
|
|
31
34
|
showHeaderAvatar: appConfig.showHeaderAvatar,
|
|
32
35
|
siteAvatarUrl: appConfig.siteAvatarUrl,
|
|
@@ -94,8 +94,10 @@ describe("Internal site admin routes", () => {
|
|
|
94
94
|
|
|
95
95
|
expect(siteRows).toEqual([{ key: "demo-cloud" }]);
|
|
96
96
|
expect(domainRows).toEqual([{ host: "demo-cloud.example.com" }]);
|
|
97
|
+
// Provisioned, not completed: the site is real and servable, but its
|
|
98
|
+
// owner has not yet confirmed the language they write in.
|
|
97
99
|
expect(settingRows).toEqual([
|
|
98
|
-
{ key: "ONBOARDING_STATUS", value: "
|
|
100
|
+
{ key: "ONBOARDING_STATUS", value: "provisioned" },
|
|
99
101
|
{ key: "SITE_LANGUAGE", value: "en" },
|
|
100
102
|
{ key: "SITE_NAME", value: "Demo Cloud" },
|
|
101
103
|
]);
|
|
@@ -136,7 +138,7 @@ describe("Internal site admin routes", () => {
|
|
|
136
138
|
.all(body.siteId) as { key: string; value: string }[];
|
|
137
139
|
|
|
138
140
|
expect(settingRows).toEqual([
|
|
139
|
-
{ key: "ONBOARDING_STATUS", value: "
|
|
141
|
+
{ key: "ONBOARDING_STATUS", value: "provisioned" },
|
|
140
142
|
{ key: "SITE_LANGUAGE", value: "en" },
|
|
141
143
|
{ key: "SITE_NAME", value: "Locale Demo" },
|
|
142
144
|
{ key: "TIME_ZONE", value: "Etc/GMT-8" },
|
|
@@ -405,6 +407,28 @@ describe("Internal site admin routes", () => {
|
|
|
405
407
|
VALUES (?, 'SITE_AVATAR', 'sites/${siteId}/avatar.webp', 1774200003)`,
|
|
406
408
|
)
|
|
407
409
|
.run(siteId);
|
|
410
|
+
// A smart collection filtering by a collection on the same site. The pair
|
|
411
|
+
// is the shape this endpoint used to die on: the condition column once
|
|
412
|
+
// carried `ON DELETE restrict`, so clearing `collection` threw partway
|
|
413
|
+
// through a delete that runs without a transaction on SQLite and D1.
|
|
414
|
+
sqlite
|
|
415
|
+
.prepare(
|
|
416
|
+
`INSERT INTO "collection" ("id", "site_id", "title", "sort_order", "created_at", "updated_at")
|
|
417
|
+
VALUES ('col_delete_1', ?, 'Notes', 'newest', 1774200004, 1774200004)`,
|
|
418
|
+
)
|
|
419
|
+
.run(siteId);
|
|
420
|
+
sqlite
|
|
421
|
+
.prepare(
|
|
422
|
+
`INSERT INTO "smart_collection" ("id", "site_id", "title", "collection_id", "format", "sort", "created_at", "updated_at")
|
|
423
|
+
VALUES ('smc_delete_1', ?, 'Untitled notes', 'col_delete_1', 'note', 'newest', 1774200005, 1774200005)`,
|
|
424
|
+
)
|
|
425
|
+
.run(siteId);
|
|
426
|
+
sqlite
|
|
427
|
+
.prepare(
|
|
428
|
+
`INSERT INTO "path_registry" ("id", "site_id", "path", "kind", "smart_collection_id", "created_at", "updated_at")
|
|
429
|
+
VALUES ('pth_delete_1', ?, 'untitled-notes', 'slug', 'smc_delete_1', 1774200006, 1774200006)`,
|
|
430
|
+
)
|
|
431
|
+
.run(siteId);
|
|
408
432
|
|
|
409
433
|
const deleteRes = await app.request(`/api/internal/sites/${siteId}`, {
|
|
410
434
|
method: "DELETE",
|
|
@@ -436,6 +460,19 @@ describe("Internal site admin routes", () => {
|
|
|
436
460
|
'SELECT COUNT(*) AS count FROM "site_setting" WHERE "site_id" = ?',
|
|
437
461
|
)
|
|
438
462
|
.get(siteId) as { count: number };
|
|
463
|
+
const deletedCollectionCount = sqlite
|
|
464
|
+
.prepare('SELECT COUNT(*) AS count FROM "collection" WHERE "site_id" = ?')
|
|
465
|
+
.get(siteId) as { count: number };
|
|
466
|
+
const deletedSmartCollectionCount = sqlite
|
|
467
|
+
.prepare(
|
|
468
|
+
'SELECT COUNT(*) AS count FROM "smart_collection" WHERE "site_id" = ?',
|
|
469
|
+
)
|
|
470
|
+
.get(siteId) as { count: number };
|
|
471
|
+
const deletedPathCount = sqlite
|
|
472
|
+
.prepare(
|
|
473
|
+
'SELECT COUNT(*) AS count FROM "path_registry" WHERE "site_id" = ?',
|
|
474
|
+
)
|
|
475
|
+
.get(siteId) as { count: number };
|
|
439
476
|
const defaultSiteCount = sqlite
|
|
440
477
|
.prepare('SELECT COUNT(*) AS count FROM "site" WHERE "id" = ?')
|
|
441
478
|
.get(DEFAULT_TEST_SITE_ID) as { count: number };
|
|
@@ -445,6 +482,9 @@ describe("Internal site admin routes", () => {
|
|
|
445
482
|
expect(deletedMemberCount.count).toBe(0);
|
|
446
483
|
expect(deletedPostCount.count).toBe(0);
|
|
447
484
|
expect(deletedSettingsCount.count).toBe(0);
|
|
485
|
+
expect(deletedCollectionCount.count).toBe(0);
|
|
486
|
+
expect(deletedSmartCollectionCount.count).toBe(0);
|
|
487
|
+
expect(deletedPathCount.count).toBe(0);
|
|
448
488
|
expect(defaultSiteCount.count).toBe(1);
|
|
449
489
|
});
|
|
450
490
|
|
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
parseValidated,
|
|
17
17
|
} from "../../lib/schemas.js";
|
|
18
18
|
import { assertFound, parseIdParam, NotFoundError } from "../../lib/errors.js";
|
|
19
|
+
import { AddressQuerySchema, requestInternalPath } from "../../lib/address.js";
|
|
20
|
+
import { toPublicPath } from "../../lib/url.js";
|
|
19
21
|
import { ID_PREFIX } from "../../lib/ids.js";
|
|
20
|
-
import { getCollectionPagePath } from "../../lib/collection-paths.js";
|
|
21
22
|
import { renderSiteHeaderHtml } from "../../lib/site-header-fragment.js";
|
|
22
23
|
|
|
23
24
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
@@ -69,6 +70,51 @@ navItemsApiRoutes.get("/pages", requireAuthApi(), async (c) => {
|
|
|
69
70
|
return c.json({ pages });
|
|
70
71
|
});
|
|
71
72
|
|
|
73
|
+
/**
|
|
74
|
+
* What a pasted address could become in navigation.
|
|
75
|
+
*
|
|
76
|
+
* The author is holding a URL, not a title, so searching titles cannot answer
|
|
77
|
+
* them. The rules stay in the service; this only says which address was asked
|
|
78
|
+
* about, so the picker can name it back.
|
|
79
|
+
*/
|
|
80
|
+
navItemsApiRoutes.get("/resolve", requireAuthApi(), async (c) => {
|
|
81
|
+
const { url } = parseValidated(AddressQuerySchema, c.req.query());
|
|
82
|
+
const path = requestInternalPath(c, url);
|
|
83
|
+
if (path === null) {
|
|
84
|
+
return c.json({ resolution: { kind: "external", address: url.trim() } });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const resolution = await c.var.services.navItems.resolveNavTarget(path);
|
|
88
|
+
const address = toPublicPath(path, c.var.appConfig.sitePathPrefix);
|
|
89
|
+
|
|
90
|
+
if (resolution.status === "page") {
|
|
91
|
+
return c.json({
|
|
92
|
+
resolution: { kind: "page", address, page: resolution.page },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
if (resolution.status === "collection") {
|
|
96
|
+
return c.json({
|
|
97
|
+
resolution: {
|
|
98
|
+
kind: "collection",
|
|
99
|
+
address,
|
|
100
|
+
collection: resolution.collection,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (resolution.status === "smart_collection") {
|
|
106
|
+
return c.json({
|
|
107
|
+
resolution: {
|
|
108
|
+
kind: "smart_collection",
|
|
109
|
+
address,
|
|
110
|
+
smartCollection: resolution.smartCollection,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return c.json({ resolution: { kind: resolution.status, address } });
|
|
116
|
+
});
|
|
117
|
+
|
|
72
118
|
// Move nav item (requires auth) — must be before /:id
|
|
73
119
|
navItemsApiRoutes.put("/:id/move", requireAuthApi(), async (c) => {
|
|
74
120
|
const id = parseIdParam(c.req.param("id"), ID_PREFIX.navItem);
|
|
@@ -98,16 +144,17 @@ navItemsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
98
144
|
placement: body.placement,
|
|
99
145
|
});
|
|
100
146
|
} else if (body.type === "collection") {
|
|
101
|
-
const collection = await c.var.services.collections.getById(
|
|
102
|
-
body.collectionId,
|
|
103
|
-
);
|
|
104
|
-
if (!collection) throw new NotFoundError("Collection");
|
|
105
|
-
|
|
106
147
|
item = await c.var.services.navItems.create({
|
|
107
148
|
type: "collection",
|
|
108
149
|
collectionId: body.collectionId,
|
|
109
|
-
label: body.label
|
|
110
|
-
|
|
150
|
+
label: body.label,
|
|
151
|
+
placement: body.placement,
|
|
152
|
+
});
|
|
153
|
+
} else if (body.type === "smart_collection") {
|
|
154
|
+
item = await c.var.services.navItems.create({
|
|
155
|
+
type: "smart_collection",
|
|
156
|
+
smartCollectionId: body.smartCollectionId,
|
|
157
|
+
label: body.label,
|
|
111
158
|
placement: body.placement,
|
|
112
159
|
});
|
|
113
160
|
} else if (body.type === "page") {
|
package/src/routes/api/posts.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { Bindings } from "../../types.js";
|
|
|
7
7
|
import type { AppVariables } from "../../types/app-context.js";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import {
|
|
10
|
+
ContentLanguageSchema,
|
|
10
11
|
CreatePostApiSchema,
|
|
11
12
|
UpdatePostApiSchema,
|
|
12
13
|
FormatSchema,
|
|
@@ -16,7 +17,10 @@ import {
|
|
|
16
17
|
} from "../../lib/schemas.js";
|
|
17
18
|
import { requireAuthApi } from "../../middleware/auth.js";
|
|
18
19
|
import { toApiAttachment, toApiPost } from "../../lib/api-posts.js";
|
|
20
|
+
import { getPostDisplayTitle } from "../../lib/post-meta.js";
|
|
19
21
|
import { assertFound, NotFoundError, parseIdParam } from "../../lib/errors.js";
|
|
22
|
+
import { AddressQuerySchema, requestInternalPath } from "../../lib/address.js";
|
|
23
|
+
import { toPublicPath } from "../../lib/url.js";
|
|
20
24
|
import { ID_PREFIX } from "../../lib/ids.js";
|
|
21
25
|
import { triggerGitHubSyncInline } from "../../lib/github-sync-trigger.js";
|
|
22
26
|
|
|
@@ -196,6 +200,8 @@ postsApiRoutes.post("/", requireAuthApi(), async (c) => {
|
|
|
196
200
|
collectionEntries: body.collectionEntries,
|
|
197
201
|
replyToId: body.replyToId,
|
|
198
202
|
quietReply: body.quietReply,
|
|
203
|
+
language: body.language,
|
|
204
|
+
translationOfId: body.translationOfId,
|
|
199
205
|
publishedAt: body.publishedAt,
|
|
200
206
|
},
|
|
201
207
|
body.attachments,
|
|
@@ -269,6 +275,7 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
269
275
|
collectionIds: body.collectionIds,
|
|
270
276
|
collectionEntries: body.collectionEntries,
|
|
271
277
|
publishedAt: body.publishedAt,
|
|
278
|
+
language: body.language,
|
|
272
279
|
},
|
|
273
280
|
body.attachments,
|
|
274
281
|
{
|
|
@@ -313,6 +320,158 @@ postsApiRoutes.put("/:id", requireAuthApi(), async (c) => {
|
|
|
313
320
|
);
|
|
314
321
|
});
|
|
315
322
|
|
|
323
|
+
// =============================================================================
|
|
324
|
+
// Language and translations
|
|
325
|
+
// =============================================================================
|
|
326
|
+
|
|
327
|
+
const SetLanguageSchema = z.object({ language: ContentLanguageSchema });
|
|
328
|
+
const TranslationCandidatesQuerySchema = z.object({
|
|
329
|
+
q: z.string().trim().min(1).max(200),
|
|
330
|
+
limit: z.coerce.number().int().min(1).max(20).optional().default(8),
|
|
331
|
+
});
|
|
332
|
+
const LinkTranslationSchema = z.object({ postId: PostIdSchema });
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Set the content language of a whole Thread.
|
|
336
|
+
*
|
|
337
|
+
* Thread-wide because `post.language` is uniform inside a Thread — see the
|
|
338
|
+
* service method for why every language filter depends on that.
|
|
339
|
+
*/
|
|
340
|
+
postsApiRoutes.put("/:id/language", requireAuthApi(), async (c) => {
|
|
341
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.post);
|
|
342
|
+
const { language } = parseValidated(SetLanguageSchema, await c.req.json());
|
|
343
|
+
|
|
344
|
+
await c.var.services.posts.setThreadLanguage(id, language);
|
|
345
|
+
await triggerGitHubSyncInline(c);
|
|
346
|
+
|
|
347
|
+
return c.json({ success: true, language });
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
/** List the Thread roots this post is a translation of. */
|
|
351
|
+
postsApiRoutes.get("/:id/translations", requireAuthApi(), async (c) => {
|
|
352
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.post);
|
|
353
|
+
const post = assertFound(await c.var.services.posts.getById(id), "Post");
|
|
354
|
+
const translations = await c.var.services.posts.listTranslations(
|
|
355
|
+
post.threadId,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
return c.json({
|
|
359
|
+
translations: translations.map((translation) => ({
|
|
360
|
+
id: translation.id,
|
|
361
|
+
slug: translation.slug,
|
|
362
|
+
title: translation.title,
|
|
363
|
+
// What to show in a list: notes are usually untitled, and a slug is not
|
|
364
|
+
// a name.
|
|
365
|
+
label: getPostDisplayTitle(translation) || translation.slug,
|
|
366
|
+
language: translation.language,
|
|
367
|
+
})),
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Posts this one could be linked to as a translation.
|
|
373
|
+
*
|
|
374
|
+
* The eligibility rules live in the service; the menu only renders what comes
|
|
375
|
+
* back, so it never offers a post the link would refuse.
|
|
376
|
+
*/
|
|
377
|
+
postsApiRoutes.get(
|
|
378
|
+
"/:id/translations/candidates",
|
|
379
|
+
requireAuthApi(),
|
|
380
|
+
async (c) => {
|
|
381
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.post);
|
|
382
|
+
const { q, limit } = parseValidated(
|
|
383
|
+
TranslationCandidatesQuerySchema,
|
|
384
|
+
c.req.query(),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const candidates = await c.var.services.posts.listTranslationCandidates(
|
|
388
|
+
id,
|
|
389
|
+
{
|
|
390
|
+
query: q,
|
|
391
|
+
limit,
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
return c.json({
|
|
396
|
+
candidates: candidates.map((post) => ({
|
|
397
|
+
id: post.id,
|
|
398
|
+
slug: post.slug,
|
|
399
|
+
title: post.title,
|
|
400
|
+
label: getPostDisplayTitle(post) || post.slug,
|
|
401
|
+
language: post.language,
|
|
402
|
+
})),
|
|
403
|
+
});
|
|
404
|
+
},
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The Thread a pasted address names, and whether it can be linked to this one.
|
|
409
|
+
*
|
|
410
|
+
* Separate from the candidate search so a URL never quietly becomes search
|
|
411
|
+
* words: this endpoint only ever answers about one address, and answers with a
|
|
412
|
+
* reason when the Thread it names is not eligible.
|
|
413
|
+
*/
|
|
414
|
+
postsApiRoutes.get("/:id/translations/resolve", requireAuthApi(), async (c) => {
|
|
415
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.post);
|
|
416
|
+
const { url } = parseValidated(AddressQuerySchema, c.req.query());
|
|
417
|
+
|
|
418
|
+
const path = requestInternalPath(c, url);
|
|
419
|
+
if (path === null) {
|
|
420
|
+
return c.json({ resolution: { kind: "external", address: url.trim() } });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const resolution = await c.var.services.posts.resolveTranslationCandidate(
|
|
424
|
+
id,
|
|
425
|
+
path,
|
|
426
|
+
);
|
|
427
|
+
const address = toPublicPath(path, c.var.appConfig.sitePathPrefix);
|
|
428
|
+
|
|
429
|
+
if (resolution.status === "ok") {
|
|
430
|
+
const post = resolution.post;
|
|
431
|
+
return c.json({
|
|
432
|
+
resolution: {
|
|
433
|
+
kind: "ok",
|
|
434
|
+
address,
|
|
435
|
+
candidate: {
|
|
436
|
+
id: post.id,
|
|
437
|
+
slug: post.slug,
|
|
438
|
+
label: getPostDisplayTitle(post) || post.slug,
|
|
439
|
+
language: post.language,
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return c.json({
|
|
446
|
+
resolution: {
|
|
447
|
+
kind: resolution.status,
|
|
448
|
+
address,
|
|
449
|
+
...("language" in resolution ? { language: resolution.language } : {}),
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
/** Link two already-published Threads as translations of each other. */
|
|
455
|
+
postsApiRoutes.post("/:id/translations", requireAuthApi(), async (c) => {
|
|
456
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.post);
|
|
457
|
+
const { postId } = parseValidated(LinkTranslationSchema, await c.req.json());
|
|
458
|
+
|
|
459
|
+
await c.var.services.posts.linkTranslation(id, postId);
|
|
460
|
+
await triggerGitHubSyncInline(c);
|
|
461
|
+
|
|
462
|
+
return c.json({ success: true });
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
/** Take this Thread out of its translation group. */
|
|
466
|
+
postsApiRoutes.delete("/:id/translations", requireAuthApi(), async (c) => {
|
|
467
|
+
const id = parseIdParam(c.req.param("id"), ID_PREFIX.post);
|
|
468
|
+
|
|
469
|
+
await c.var.services.posts.unlinkTranslation(id);
|
|
470
|
+
await triggerGitHubSyncInline(c);
|
|
471
|
+
|
|
472
|
+
return c.json({ success: true });
|
|
473
|
+
});
|
|
474
|
+
|
|
316
475
|
// Delete post (requires auth)
|
|
317
476
|
postsApiRoutes.delete("/:id", requireAuthApi(), async (c) => {
|
|
318
477
|
const id = parseIdParam(c.req.param("id"), ID_PREFIX.post);
|