@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
|
@@ -1,183 +1,836 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Setup Routes
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* First-run setup, for both ways a Jant site comes into existence.
|
|
5
|
+
*
|
|
6
|
+
* A self-hosted site starts empty and is set up in two screens: the account
|
|
7
|
+
* first, then the site. A hosted site is created by a control plane, which
|
|
8
|
+
* already knows the name and the owner but can only guess at the one thing
|
|
9
|
+
* that shows up in public HTML, feeds and font stacks — the language its
|
|
10
|
+
* author writes in — so it arrives at the second screen and stops there.
|
|
11
|
+
*
|
|
12
|
+
* The split is what makes that true. Between the screens the site stands
|
|
13
|
+
* provisioned with an owner and no answers, which is exactly the state a
|
|
14
|
+
* control plane leaves a hosted site in, so the second screen is one code path
|
|
15
|
+
* asking one site's worth of questions rather than two forms drifting apart.
|
|
16
|
+
* Which step a request is on is never in the URL: it is read from the
|
|
17
|
+
* onboarding status and the session, the only record that survives a closed
|
|
18
|
+
* tab. A `provisioned` site also serves `/signin` normally, so an author who
|
|
19
|
+
* loses the session between the screens signs back in and lands on the second.
|
|
20
|
+
* The first screen is resumable in its own way, for the run that failed partway
|
|
21
|
+
* through it — see `openOwnerSession`, which is the only way out of that one.
|
|
22
|
+
*
|
|
23
|
+
* The cost of standing the site up early is that the public root answers for
|
|
24
|
+
* the width of one screen under the fallback name. On a first run there is
|
|
25
|
+
* nothing there to read, and the alternative was a second "does an owner
|
|
26
|
+
* exist" query wired into the onboarding middleware.
|
|
27
|
+
*
|
|
28
|
+
* Asking little is not the same as saying nothing. A hosted author reaches
|
|
29
|
+
* this page by following a link out of a control plane and lands on a domain
|
|
30
|
+
* they have never seen serve anything, so the screen names the step and the
|
|
31
|
+
* site it belongs to. A self-hosted author has no site name to be told, and
|
|
32
|
+
* gets the count of what is left instead. That is the whole of it: one muted
|
|
33
|
+
* line above the question, no mark and no status summary, because chrome is
|
|
34
|
+
* what made a one-field form look like a gate in the first place. The counter
|
|
35
|
+
* is text and not a stepper on purpose — once the first screen is submitted
|
|
36
|
+
* the account exists and there is no going back, and anything that looked like
|
|
37
|
+
* navigation would say otherwise.
|
|
5
38
|
*/
|
|
6
39
|
|
|
7
40
|
import { Hono } from "hono";
|
|
8
|
-
import type {
|
|
41
|
+
import type { Context } from "hono";
|
|
42
|
+
import type { Child, FC, PropsWithChildren } from "hono/jsx";
|
|
9
43
|
import { msg } from "@lingui/core/macro";
|
|
10
44
|
import { useLingui } from "../../i18n/context.js";
|
|
11
45
|
import type { Bindings } from "../../types.js";
|
|
12
46
|
import type { AppVariables } from "../../types/app-context.js";
|
|
13
47
|
import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
|
|
14
48
|
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
15
|
-
import {
|
|
49
|
+
import type { ZodError } from "zod";
|
|
50
|
+
import {
|
|
51
|
+
deriveAccountName,
|
|
52
|
+
SetupAccountSchema,
|
|
53
|
+
SetupLanguageSchema,
|
|
54
|
+
SetupSiteSchema,
|
|
55
|
+
type SetupLanguageAnswers,
|
|
56
|
+
} from "../../lib/schemas.js";
|
|
16
57
|
import { buildPageTitle } from "../../lib/page-title.js";
|
|
17
58
|
import { mapIanaToTimezone } from "../../lib/timezones.js";
|
|
18
59
|
import { getI18n } from "../../i18n/index.js";
|
|
60
|
+
import { isCurrentSiteMember } from "../../middleware/auth.js";
|
|
61
|
+
import type { I18n } from "../../i18n/i18n.js";
|
|
19
62
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from "../../i18n/
|
|
63
|
+
getSupportedLocaleEntries,
|
|
64
|
+
resolveSupportedLocaleTag,
|
|
65
|
+
} from "../../i18n/supported-locales.js";
|
|
23
66
|
import { toPublicPath } from "../../lib/url.js";
|
|
67
|
+
import { ONBOARDING_STATUS } from "../../lib/constants.js";
|
|
68
|
+
import { announceInBackground } from "../discover-announce.js";
|
|
69
|
+
import {
|
|
70
|
+
discoverIntroRuns,
|
|
71
|
+
getDiscoverPageUrls,
|
|
72
|
+
resolveDiscoverMode,
|
|
73
|
+
type DiscoverPageUrls,
|
|
74
|
+
} from "../../lib/discover.js";
|
|
75
|
+
import {
|
|
76
|
+
getDiscoverDefault,
|
|
77
|
+
getDiscoverDirectoryBaseUrl,
|
|
78
|
+
} from "../../lib/env.js";
|
|
79
|
+
import {
|
|
80
|
+
getDiscoverCopy,
|
|
81
|
+
type DiscoverCopy,
|
|
82
|
+
} from "../../ui/dash/settings/discover-copy.js";
|
|
24
83
|
|
|
25
84
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
26
85
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The language field, as a plain `<select>` plus the picker that replaces it.
|
|
88
|
+
*
|
|
89
|
+
* The select is the form's real control and stays so; the picker takes over on
|
|
90
|
+
* upgrade and writes back to it. Stylesheet rule `jant-locale-picker:not(:defined)`
|
|
91
|
+
* keeps the picker out of the layout until then, so only one control is ever
|
|
92
|
+
* visible.
|
|
93
|
+
*/
|
|
94
|
+
const LocaleField: FC<{
|
|
95
|
+
id: string;
|
|
96
|
+
labelId: string;
|
|
97
|
+
contentLanguage: string;
|
|
98
|
+
searchLabel: string;
|
|
99
|
+
emptyLabel: string;
|
|
100
|
+
}> = ({ id, labelId, contentLanguage, searchLabel, emptyLabel }) => {
|
|
101
|
+
const entries = getSupportedLocaleEntries();
|
|
102
|
+
const locales = JSON.stringify(
|
|
103
|
+
entries.map((entry) => ({
|
|
104
|
+
tag: entry.tag,
|
|
105
|
+
native: entry.native,
|
|
106
|
+
english: entry.english,
|
|
107
|
+
coverage: entry.coverage,
|
|
108
|
+
})),
|
|
109
|
+
).replace(/</g, "\\u003c");
|
|
110
|
+
const labels = JSON.stringify({
|
|
111
|
+
search: searchLabel,
|
|
112
|
+
empty: emptyLabel,
|
|
113
|
+
}).replace(/</g, "\\u003c");
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
{/* The real form field, and the whole control until the picker upgrades.
|
|
118
|
+
Datastar binds to it, so the picker writing here is enough — it never
|
|
119
|
+
needs to know a signal exists. */}
|
|
120
|
+
<select id={id} data-bind="contentLanguage" class="select">
|
|
121
|
+
{entries.map((entry) => (
|
|
122
|
+
<option
|
|
123
|
+
key={entry.tag}
|
|
124
|
+
value={entry.tag}
|
|
125
|
+
selected={entry.tag === contentLanguage}
|
|
126
|
+
>
|
|
127
|
+
{entry.native === entry.english
|
|
128
|
+
? entry.native
|
|
129
|
+
: `${entry.native} (${entry.english})`}
|
|
130
|
+
</option>
|
|
131
|
+
))}
|
|
132
|
+
</select>
|
|
133
|
+
<jant-locale-picker
|
|
134
|
+
for={id}
|
|
135
|
+
labelledby={labelId}
|
|
136
|
+
value={contentLanguage}
|
|
137
|
+
locales={locales}
|
|
138
|
+
labels={labels}
|
|
139
|
+
full-width
|
|
140
|
+
/>
|
|
141
|
+
</>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The one question setup asks about the world outside this site.
|
|
147
|
+
*
|
|
148
|
+
* It earns a place on a screen that asks as little as it can because the
|
|
149
|
+
* setting behind it is otherwise three clicks into Settings, and an author who
|
|
150
|
+
* never goes looking never learns a directory exists. The wording is the
|
|
151
|
+
* settings page's own: one control appearing twice should not describe itself
|
|
152
|
+
* two ways.
|
|
153
|
+
*
|
|
154
|
+
* The words come from the same `getDiscoverCopy` the settings page uses,
|
|
155
|
+
* which is what keeps the two surfaces from drifting when either is edited.
|
|
156
|
+
* The label is the settings page's; the help line is the settings page's
|
|
157
|
+
* opening sentence and nothing after it. The delay, the lists and the rules
|
|
158
|
+
* matter to an author with posts to show, and this screen comes before the
|
|
159
|
+
* first one — the directory's name links to the directory for anyone who wants
|
|
160
|
+
* more. Where to find the setting again is left to the screen's footnote, which
|
|
161
|
+
* says it once for every answer on it.
|
|
162
|
+
*/
|
|
163
|
+
const DiscoverField: FC<{
|
|
164
|
+
copy: DiscoverCopy;
|
|
165
|
+
/** The directory's pages, or null when this deployment announces to none. */
|
|
166
|
+
pages: DiscoverPageUrls | null;
|
|
167
|
+
}> = ({ copy, pages }) => {
|
|
168
|
+
const runs = discoverIntroRuns(copy.about, copy, pages);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
// Held a little further from the field above it than the form's own gap:
|
|
172
|
+
// every other field on this screen is about the site itself, and this one
|
|
173
|
+
// is about the world outside it.
|
|
174
|
+
<div class="field mt-2">
|
|
175
|
+
{/* Classes copied from the settings page's own Discover checkbox
|
|
176
|
+
(`jant-settings-general.ts`), so the control a hosted author meets here
|
|
177
|
+
and the one they find later in Settings are the same object. */}
|
|
178
|
+
<label
|
|
179
|
+
class="flex items-center gap-2 cursor-pointer"
|
|
180
|
+
for="setup-discover"
|
|
181
|
+
>
|
|
182
|
+
<input
|
|
183
|
+
id="setup-discover"
|
|
184
|
+
type="checkbox"
|
|
185
|
+
data-bind="discover"
|
|
186
|
+
class="checkbox"
|
|
187
|
+
/>
|
|
188
|
+
<span>{copy.label}</span>
|
|
189
|
+
</label>
|
|
190
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
191
|
+
{runs.map((run) =>
|
|
192
|
+
run.href ? (
|
|
193
|
+
<a
|
|
194
|
+
href={run.href}
|
|
195
|
+
target="_blank"
|
|
196
|
+
rel="noopener noreferrer"
|
|
197
|
+
class="underline hover:text-foreground transition-colors"
|
|
198
|
+
>
|
|
199
|
+
{run.text}
|
|
200
|
+
</a>
|
|
201
|
+
) : (
|
|
202
|
+
run.text
|
|
203
|
+
),
|
|
204
|
+
)}
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* What this screen is called, in the tab title and above the card alike, so
|
|
212
|
+
* the two never drift into naming the same page differently.
|
|
213
|
+
*/
|
|
214
|
+
function setupLabel(i18n: I18n): string {
|
|
215
|
+
return i18n._(
|
|
216
|
+
msg({
|
|
217
|
+
message: "Setup",
|
|
218
|
+
comment:
|
|
219
|
+
"@context: Name of the first-run setup screen — shown above the setup card and as the browser tab title",
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** How many screens a self-hosted setup has, and which one this is. */
|
|
225
|
+
const SETUP_STEPS = 2;
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The frame every first-run screen shares.
|
|
229
|
+
*
|
|
230
|
+
* One muted line carries everything these screens were missing: which step
|
|
231
|
+
* this is, and which site it belongs to. A mark, an address, and a summary of
|
|
232
|
+
* what the control plane already answered were all tried above it and all read
|
|
233
|
+
* as chrome stacked around a form with one field in it.
|
|
234
|
+
*
|
|
235
|
+
* The two facts occupy the same line because no screen ever has both: a
|
|
236
|
+
* self-hosted install is counting steps and has no name yet, and a hosted one
|
|
237
|
+
* has a name and only ever sees a single screen. They are joined rather than
|
|
238
|
+
* chosen between anyway, so a future screen that has both says both.
|
|
239
|
+
*/
|
|
240
|
+
const SetupShell: FC<
|
|
241
|
+
PropsWithChildren<{
|
|
242
|
+
/** Site being set up. Blank while it has no name yet. */
|
|
243
|
+
siteName?: string;
|
|
244
|
+
/** Which of the self-hosted screens this is. Absent on a hosted site. */
|
|
245
|
+
step?: number;
|
|
246
|
+
heading: string;
|
|
247
|
+
/** What this screen is for, under the heading. Omitted where the heading says it. */
|
|
248
|
+
description?: string;
|
|
249
|
+
/**
|
|
250
|
+
* A note that belongs to the screen but not to the question — set in the
|
|
251
|
+
* card's footer, after the button, where it is read once the answers are
|
|
252
|
+
* given rather than before.
|
|
253
|
+
*/
|
|
254
|
+
footnote?: string;
|
|
255
|
+
}>
|
|
256
|
+
> = ({ siteName, step, heading, description, footnote, children }) => {
|
|
30
257
|
const { i18n } = useLingui();
|
|
258
|
+
const name = siteName?.trim() ?? "";
|
|
259
|
+
const parts = [setupLabel(i18n)];
|
|
260
|
+
if (step) {
|
|
261
|
+
parts.push(
|
|
262
|
+
i18n._(
|
|
263
|
+
msg({
|
|
264
|
+
message: "Step {current} of {total}",
|
|
265
|
+
comment:
|
|
266
|
+
"@context: How far along first-run setup is, shown beside the screen's name on a self-hosted install",
|
|
267
|
+
}),
|
|
268
|
+
{ current: step, total: SETUP_STEPS },
|
|
269
|
+
),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
if (name) parts.push(name);
|
|
31
273
|
|
|
32
274
|
return (
|
|
33
|
-
<div class="min-h-screen flex items-center justify-center">
|
|
275
|
+
<div class="min-h-screen flex items-center justify-center p-4">
|
|
34
276
|
<div class="card max-w-md w-full">
|
|
35
277
|
<header>
|
|
36
|
-
<
|
|
278
|
+
<p class="mb-2 text-sm text-muted-foreground">{parts.join(" · ")}</p>
|
|
279
|
+
<h2>{heading}</h2>
|
|
280
|
+
{description ? <p>{description}</p> : null}
|
|
281
|
+
</header>
|
|
282
|
+
<section>{children}</section>
|
|
283
|
+
{footnote ? (
|
|
284
|
+
<footer>
|
|
285
|
+
<p class="text-sm text-muted-foreground">{footnote}</p>
|
|
286
|
+
</footer>
|
|
287
|
+
) : null}
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The first self-hosted screen: the account, and nothing about the site.
|
|
295
|
+
*
|
|
296
|
+
* Credentials alone is not only less to read: an email and a password with
|
|
297
|
+
* nothing between them is the shape a password manager is looking for, which
|
|
298
|
+
* a form that opens with the site's name and language is not.
|
|
299
|
+
*/
|
|
300
|
+
const AccountStep: FC<{ action: string; spinner: Child }> = ({
|
|
301
|
+
action,
|
|
302
|
+
spinner,
|
|
303
|
+
}) => {
|
|
304
|
+
const { i18n } = useLingui();
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<SetupShell
|
|
308
|
+
step={1}
|
|
309
|
+
heading={i18n._(
|
|
310
|
+
msg({
|
|
311
|
+
message: "Welcome to Jant",
|
|
312
|
+
comment: "@context: Setup page welcome heading",
|
|
313
|
+
}),
|
|
314
|
+
)}
|
|
315
|
+
description={i18n._(
|
|
316
|
+
msg({
|
|
317
|
+
message: "Create the account you write from.",
|
|
318
|
+
comment:
|
|
319
|
+
"@context: Setup page description on the first screen, which asks only for credentials",
|
|
320
|
+
}),
|
|
321
|
+
)}
|
|
322
|
+
>
|
|
323
|
+
<form
|
|
324
|
+
data-signals="{email: '', password: ''}"
|
|
325
|
+
data-on:submit__prevent={action}
|
|
326
|
+
data-indicator="_loading"
|
|
327
|
+
class="flex flex-col gap-4"
|
|
328
|
+
>
|
|
329
|
+
<div class="field">
|
|
330
|
+
<label class="label" for="setup-email">
|
|
37
331
|
{i18n._(
|
|
38
332
|
msg({
|
|
39
|
-
message: "
|
|
40
|
-
comment: "@context: Setup
|
|
333
|
+
message: "Email",
|
|
334
|
+
comment: "@context: Setup/signin form field - email",
|
|
41
335
|
}),
|
|
42
336
|
)}
|
|
43
|
-
</
|
|
44
|
-
<
|
|
337
|
+
</label>
|
|
338
|
+
<input
|
|
339
|
+
id="setup-email"
|
|
340
|
+
type="email"
|
|
341
|
+
data-bind="email"
|
|
342
|
+
class="input"
|
|
343
|
+
required
|
|
344
|
+
autocomplete="username"
|
|
345
|
+
placeholder="you@example.com"
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
<div class="field">
|
|
349
|
+
<label class="label" for="setup-password">
|
|
45
350
|
{i18n._(
|
|
46
351
|
msg({
|
|
47
|
-
message: "
|
|
48
|
-
comment: "@context: Setup
|
|
352
|
+
message: "Password",
|
|
353
|
+
comment: "@context: Setup/signin form field - password",
|
|
49
354
|
}),
|
|
50
355
|
)}
|
|
51
|
-
</
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
data-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
356
|
+
</label>
|
|
357
|
+
<input
|
|
358
|
+
id="setup-password"
|
|
359
|
+
type="password"
|
|
360
|
+
data-bind="password"
|
|
361
|
+
class="input"
|
|
362
|
+
required
|
|
363
|
+
autocomplete="new-password"
|
|
364
|
+
minLength={8}
|
|
365
|
+
/>
|
|
366
|
+
</div>
|
|
367
|
+
{/* Not "Complete Setup": there is another screen after this one, and a
|
|
368
|
+
button that claims otherwise is the reason the next one reads as a
|
|
369
|
+
failure rather than a step. */}
|
|
370
|
+
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
371
|
+
{spinner}
|
|
372
|
+
{i18n._(
|
|
373
|
+
msg({
|
|
374
|
+
message: "Continue",
|
|
375
|
+
comment:
|
|
376
|
+
"@context: Setup submit button on the first screen, which is followed by the site screen",
|
|
377
|
+
}),
|
|
378
|
+
)}
|
|
379
|
+
</button>
|
|
380
|
+
</form>
|
|
381
|
+
</SetupShell>
|
|
382
|
+
);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
export type SetupContentProps = {
|
|
386
|
+
sitePathPrefix?: string;
|
|
387
|
+
} & (
|
|
388
|
+
| {
|
|
389
|
+
/** The account screen, which a hosted site never sees. */
|
|
390
|
+
mode: "account";
|
|
391
|
+
}
|
|
392
|
+
| {
|
|
393
|
+
/**
|
|
394
|
+
* The site screen: the last question either install kind has left.
|
|
395
|
+
*/
|
|
396
|
+
mode: "site";
|
|
397
|
+
/**
|
|
398
|
+
* Whether the site still needs a name — true on a self-hosted install,
|
|
399
|
+
* false on a hosted one the control plane already named.
|
|
400
|
+
*
|
|
401
|
+
* Doubles as which flow this is, and so as whether the step counter
|
|
402
|
+
* shows: only a two-screen setup has a step to count.
|
|
403
|
+
*/
|
|
404
|
+
askSiteName: boolean;
|
|
405
|
+
contentLanguage: string;
|
|
406
|
+
/** Shown beside the step name once there is a name, so the site is identified. */
|
|
407
|
+
siteName?: string;
|
|
408
|
+
/**
|
|
409
|
+
* Whether to ask the Discover question at all.
|
|
410
|
+
*
|
|
411
|
+
* False where the answer could not be honoured — a demo site, or feeds
|
|
412
|
+
* switched off — and a control nobody can act on is worse than none.
|
|
413
|
+
*/
|
|
414
|
+
discoverAvailable: boolean;
|
|
415
|
+
/**
|
|
416
|
+
* The state the Discover box starts in, derived from the deployment
|
|
417
|
+
* rather than hardcoded per install kind. Hosted Jant sets
|
|
418
|
+
* `DISCOVER=latest`, so a hosted author finds it ticked; a self-hosted
|
|
419
|
+
* install with nothing configured finds it clear.
|
|
420
|
+
*/
|
|
421
|
+
discoverDefault: boolean;
|
|
422
|
+
/**
|
|
423
|
+
* The directory's pages, for the links in the help line. Null when this
|
|
424
|
+
* deployment announces to no directory, and the line is then plain text.
|
|
425
|
+
*/
|
|
426
|
+
discoverPages: DiscoverPageUrls | null;
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
export const SetupContent: FC<SetupContentProps> = (props) => {
|
|
431
|
+
const { i18n } = useLingui();
|
|
432
|
+
const action = `@post('${toPublicPath("/setup", props.sitePathPrefix ?? "")}')`;
|
|
433
|
+
const spinner = (
|
|
434
|
+
<svg
|
|
435
|
+
data-show="$_loading"
|
|
436
|
+
style="display:none"
|
|
437
|
+
class="animate-spin size-4"
|
|
438
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
439
|
+
viewBox="0 0 24 24"
|
|
440
|
+
fill="none"
|
|
441
|
+
stroke="currentColor"
|
|
442
|
+
stroke-width="2"
|
|
443
|
+
stroke-linecap="round"
|
|
444
|
+
stroke-linejoin="round"
|
|
445
|
+
role="status"
|
|
446
|
+
>
|
|
447
|
+
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
|
448
|
+
</svg>
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
if (props.mode === "account") {
|
|
452
|
+
return <AccountStep action={action} spinner={spinner} />;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const {
|
|
456
|
+
askSiteName,
|
|
457
|
+
contentLanguage,
|
|
458
|
+
siteName,
|
|
459
|
+
discoverAvailable,
|
|
460
|
+
discoverDefault,
|
|
461
|
+
discoverPages,
|
|
462
|
+
} = props;
|
|
463
|
+
|
|
464
|
+
const searchLabel = i18n._(
|
|
465
|
+
msg({
|
|
466
|
+
message: "Search…",
|
|
467
|
+
comment: "@context: Placeholder in the language picker search box",
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
const emptyLabel = i18n._(
|
|
471
|
+
msg({
|
|
472
|
+
message: "No matches.",
|
|
473
|
+
comment: "@context: Empty state in the language picker",
|
|
474
|
+
}),
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// Rendered even when the question is not asked, so the form can name the
|
|
478
|
+
// signal unconditionally; `discoverAvailable` decides whether the control
|
|
479
|
+
// appears, and an absent field simply sends the default back.
|
|
480
|
+
const discoverField = discoverAvailable ? (
|
|
481
|
+
<DiscoverField copy={getDiscoverCopy(i18n)} pages={discoverPages} />
|
|
482
|
+
) : null;
|
|
483
|
+
|
|
484
|
+
const signals = [
|
|
485
|
+
askSiteName ? "siteName: ''" : null,
|
|
486
|
+
`contentLanguage: ${JSON.stringify(contentLanguage)}`,
|
|
487
|
+
"language: ''",
|
|
488
|
+
// Only the install that is still choosing its own clock reports one. A
|
|
489
|
+
// hosted site's time zone came from the control plane, and this screen can
|
|
490
|
+
// be opened from anywhere.
|
|
491
|
+
askSiteName ? "timezone: ''" : null,
|
|
492
|
+
`discover: ${discoverAvailable && discoverDefault}`,
|
|
493
|
+
]
|
|
494
|
+
.filter(Boolean)
|
|
495
|
+
.join(", ");
|
|
496
|
+
const init = [
|
|
497
|
+
"$language = navigator.language || ''",
|
|
498
|
+
askSiteName
|
|
499
|
+
? "$timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''"
|
|
500
|
+
: null,
|
|
501
|
+
]
|
|
502
|
+
.filter(Boolean)
|
|
503
|
+
.join("; ");
|
|
504
|
+
|
|
505
|
+
return (
|
|
506
|
+
<SetupShell
|
|
507
|
+
siteName={siteName}
|
|
508
|
+
step={askSiteName ? 2 : undefined}
|
|
509
|
+
heading={
|
|
510
|
+
askSiteName
|
|
511
|
+
? i18n._(
|
|
512
|
+
msg({
|
|
513
|
+
message: "Set up your site",
|
|
514
|
+
comment:
|
|
515
|
+
"@context: Setup heading on the second self-hosted screen, which asks for the site's name and language",
|
|
516
|
+
}),
|
|
517
|
+
)
|
|
518
|
+
: i18n._(
|
|
519
|
+
msg({
|
|
520
|
+
message: "What language do you write in?",
|
|
521
|
+
comment:
|
|
522
|
+
"@context: Setup heading on a hosted site, where the language is all that is left to ask",
|
|
523
|
+
}),
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
// Not under the heading. Said there, before the questions, it reads as
|
|
527
|
+
// permission to leave them blank; said after the button, it is what it
|
|
528
|
+
// is — a note that nothing here is final.
|
|
529
|
+
footnote={i18n._(
|
|
530
|
+
msg({
|
|
531
|
+
message: "You can change all of this later in Settings.",
|
|
532
|
+
comment:
|
|
533
|
+
"@context: Note at the foot of the last setup screen, after the submit button",
|
|
534
|
+
}),
|
|
535
|
+
)}
|
|
536
|
+
>
|
|
537
|
+
<form
|
|
538
|
+
data-signals={`{${signals}}`}
|
|
539
|
+
data-init={init}
|
|
540
|
+
data-on:submit__prevent={action}
|
|
541
|
+
data-indicator="_loading"
|
|
542
|
+
class="flex flex-col gap-4"
|
|
543
|
+
>
|
|
544
|
+
{askSiteName ? (
|
|
545
|
+
<div class="field">
|
|
546
|
+
<label class="label" for="setup-site-name">
|
|
128
547
|
{i18n._(
|
|
129
548
|
msg({
|
|
130
|
-
message: "
|
|
131
|
-
comment: "@context: Setup form
|
|
549
|
+
message: "Site Name",
|
|
550
|
+
comment: "@context: Setup form field - site name",
|
|
132
551
|
}),
|
|
133
552
|
)}
|
|
134
|
-
</
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
553
|
+
</label>
|
|
554
|
+
<input
|
|
555
|
+
id="setup-site-name"
|
|
556
|
+
type="text"
|
|
557
|
+
data-bind="siteName"
|
|
558
|
+
class="input"
|
|
559
|
+
required
|
|
560
|
+
placeholder="My Blog"
|
|
561
|
+
/>
|
|
562
|
+
</div>
|
|
563
|
+
) : null}
|
|
564
|
+
{/* Asked outright rather than inferred from the browser. The inference
|
|
565
|
+
is wrong exactly for the people it matters to — anyone whose browser
|
|
566
|
+
language is not their writing language — and it silently mis-sets
|
|
567
|
+
`<html lang>`, the feed language, and the CJK font stack. */}
|
|
568
|
+
<div class="field">
|
|
569
|
+
{askSiteName ? (
|
|
570
|
+
<label class="label" id="setup-language-label">
|
|
571
|
+
{i18n._(
|
|
572
|
+
msg({
|
|
573
|
+
message: "Content language",
|
|
574
|
+
comment: "@context: Setup form field - site content language",
|
|
575
|
+
}),
|
|
576
|
+
)}
|
|
577
|
+
</label>
|
|
578
|
+
) : (
|
|
579
|
+
// The heading already asks the question, so a visible label would
|
|
580
|
+
// ask it twice.
|
|
581
|
+
<span id="setup-language-label" class="sr-only">
|
|
582
|
+
{i18n._(
|
|
583
|
+
msg({
|
|
584
|
+
message: "Content language",
|
|
585
|
+
comment: "@context: Setup form field - site content language",
|
|
586
|
+
}),
|
|
587
|
+
)}
|
|
588
|
+
</span>
|
|
589
|
+
)}
|
|
590
|
+
<LocaleField
|
|
591
|
+
id="setup-content-language"
|
|
592
|
+
labelId="setup-language-label"
|
|
593
|
+
contentLanguage={contentLanguage}
|
|
594
|
+
searchLabel={searchLabel}
|
|
595
|
+
emptyLabel={emptyLabel}
|
|
596
|
+
/>
|
|
597
|
+
{askSiteName ? (
|
|
598
|
+
<p class="text-sm text-muted-foreground mt-1">
|
|
599
|
+
{i18n._(
|
|
600
|
+
msg({
|
|
601
|
+
message: "The language your readers and search engines see.",
|
|
602
|
+
comment:
|
|
603
|
+
"@context: Setup form help text under the content language field",
|
|
604
|
+
}),
|
|
605
|
+
)}
|
|
606
|
+
</p>
|
|
607
|
+
) : null}
|
|
608
|
+
</div>
|
|
609
|
+
{discoverField}
|
|
610
|
+
<button type="submit" class="btn" data-attr:disabled="$_loading">
|
|
611
|
+
{spinner}
|
|
612
|
+
{i18n._(
|
|
613
|
+
msg({
|
|
614
|
+
message: "Start writing",
|
|
615
|
+
comment:
|
|
616
|
+
"@context: Setup submit button on the last screen, after which the site is live",
|
|
617
|
+
}),
|
|
618
|
+
)}
|
|
619
|
+
</button>
|
|
620
|
+
</form>
|
|
621
|
+
</SetupShell>
|
|
139
622
|
);
|
|
140
623
|
};
|
|
141
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Record the Discover answer the setup form carried, and act on it.
|
|
627
|
+
*
|
|
628
|
+
* Stored either way, including a refusal: from here on the author's own answer
|
|
629
|
+
* outranks the deployment's `DISCOVER` default and the `noindex` reading, which
|
|
630
|
+
* is the whole point of asking. An absent field is not a refusal — an older
|
|
631
|
+
* client or a scripted setup sends none — and leaves the default in force.
|
|
632
|
+
*
|
|
633
|
+
* A yes is announced immediately, on the same terms as the settings page's own
|
|
634
|
+
* switch. A directory decides eligibility for itself and re-reads the feed on
|
|
635
|
+
* its own schedule, so a site with nothing published yet loses nothing by
|
|
636
|
+
* saying hello early — it is on the list of addresses to look at, and the
|
|
637
|
+
* looking happens when there is something to see. Waiting instead would mean
|
|
638
|
+
* the answer only takes effect if the author later finds a settings page they
|
|
639
|
+
* have no reason to open, which is not a design, it is a leak.
|
|
640
|
+
*
|
|
641
|
+
* The announcement is not awaited, and so cannot be reported here: a directory
|
|
642
|
+
* takes up to twelve seconds to give up on, and the last screen of setup is not
|
|
643
|
+
* a place to spend them. The outcome is recorded and logged, and the settings
|
|
644
|
+
* page's status block reads it back with a Retry beside it.
|
|
645
|
+
*
|
|
646
|
+
* @param c - The setup request, for its services and its config
|
|
647
|
+
* @param answer - The checkbox, or undefined when the form carried no field
|
|
648
|
+
*/
|
|
649
|
+
async function storeDiscoverAnswer(
|
|
650
|
+
c: Context<Env>,
|
|
651
|
+
answer: boolean | undefined,
|
|
652
|
+
): Promise<void> {
|
|
653
|
+
if (answer === undefined) return;
|
|
654
|
+
const stored = answer ? "latest" : "off";
|
|
655
|
+
const { shouldAnnounce } =
|
|
656
|
+
await c.var.services.settings.updateDiscoverSetting(stored, {
|
|
657
|
+
demoMode: c.var.appConfig.demoMode,
|
|
658
|
+
});
|
|
659
|
+
if (shouldAnnounce) {
|
|
660
|
+
// The value just written, not `allSettings`: that snapshot was taken before
|
|
661
|
+
// this request ran and still holds the deployment default.
|
|
662
|
+
announceInBackground(c, stored);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Whether this site still owes setup a name of its own.
|
|
668
|
+
*
|
|
669
|
+
* The discriminator between the two ways a site reaches the second screen, and
|
|
670
|
+
* it reads the stored fact rather than a flow flag: a control plane names the
|
|
671
|
+
* site it creates, a self-hosted install does not, and nothing else can put a
|
|
672
|
+
* name there before setup closes.
|
|
673
|
+
*
|
|
674
|
+
* @param c - The setup request, for its settings service
|
|
675
|
+
* @returns True when the second screen must ask for the site's name
|
|
676
|
+
*/
|
|
677
|
+
async function needsSiteName(c: Context<Env>): Promise<boolean> {
|
|
678
|
+
const stored = await c.var.services.settings.get("SITE_NAME");
|
|
679
|
+
return !stored?.trim();
|
|
680
|
+
}
|
|
681
|
+
|
|
142
682
|
export const setupRoutes = new Hono<Env>();
|
|
143
683
|
|
|
144
684
|
setupRoutes.get("/setup", async (c) => {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
685
|
+
const status = await c.var.services.settings.getOnboardingStatus();
|
|
686
|
+
const home = toPublicPath("/", c.var.appConfig.sitePathPrefix);
|
|
687
|
+
if (status === ONBOARDING_STATUS.COMPLETED) return c.redirect(home);
|
|
688
|
+
|
|
689
|
+
const i18n = getI18n(c);
|
|
690
|
+
const { appConfig } = c.var;
|
|
691
|
+
const title = buildPageTitle(setupLabel(i18n), appConfig.siteName);
|
|
692
|
+
|
|
693
|
+
if (status === ONBOARDING_STATUS.PENDING) {
|
|
694
|
+
return c.html(
|
|
695
|
+
<BaseLayout title={title} c={c}>
|
|
696
|
+
<SetupContent
|
|
697
|
+
mode="account"
|
|
698
|
+
sitePathPrefix={appConfig.sitePathPrefix}
|
|
699
|
+
/>
|
|
700
|
+
</BaseLayout>,
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// The site now exists and is perfectly readable, so the remaining question
|
|
705
|
+
// belongs to its owner alone — a visitor who is not signed in to this site is
|
|
706
|
+
// sent to the site rather than shown a form they cannot submit.
|
|
707
|
+
if (!(await isCurrentSiteMember(c))) return c.redirect(home);
|
|
708
|
+
|
|
709
|
+
const askSiteName = await needsSiteName(c);
|
|
710
|
+
|
|
711
|
+
// Read through the same derivation the feed uses, so the box shows what this
|
|
712
|
+
// site would declare if the author changed nothing. `storedValue` is null by
|
|
713
|
+
// construction: setup runs before anyone has answered.
|
|
714
|
+
const discoverDefault =
|
|
715
|
+
resolveDiscoverMode({
|
|
716
|
+
storedValue: null,
|
|
717
|
+
defaultValue: getDiscoverDefault(c.env),
|
|
718
|
+
demoMode: appConfig.demoMode,
|
|
719
|
+
noindex: appConfig.noindex,
|
|
720
|
+
rssFeedsEnabled: appConfig.rssFeedsEnabled,
|
|
721
|
+
}) !== "none";
|
|
148
722
|
|
|
149
723
|
return c.html(
|
|
150
|
-
<BaseLayout title={
|
|
151
|
-
<SetupContent
|
|
724
|
+
<BaseLayout title={title} c={c}>
|
|
725
|
+
<SetupContent
|
|
726
|
+
sitePathPrefix={appConfig.sitePathPrefix}
|
|
727
|
+
mode="site"
|
|
728
|
+
askSiteName={askSiteName}
|
|
729
|
+
// Not asked where it could not be honoured. Both locks outlive setup —
|
|
730
|
+
// a demo site is never listed, and Discover reads an Atom feed — so a
|
|
731
|
+
// ticked box here would be a promise the next screen breaks.
|
|
732
|
+
discoverAvailable={!appConfig.demoMode && appConfig.rssFeedsEnabled}
|
|
733
|
+
discoverDefault={discoverDefault}
|
|
734
|
+
// The same directory the settings page links to, derived from the same
|
|
735
|
+
// configuration, so this screen can never point at one directory while
|
|
736
|
+
// the site announces to another.
|
|
737
|
+
discoverPages={getDiscoverPageUrls(getDiscoverDirectoryBaseUrl(c.env))}
|
|
738
|
+
contentLanguage={
|
|
739
|
+
// On a named site the control plane's guess is already stored, so
|
|
740
|
+
// offering it back is offering the site's current language. On an
|
|
741
|
+
// unnamed one nothing has been stored yet, so the browser's header is
|
|
742
|
+
// the only prefill there is.
|
|
743
|
+
askSiteName
|
|
744
|
+
? resolveSupportedLocaleTag(c.req.header("Accept-Language"))
|
|
745
|
+
: appConfig.siteLanguage
|
|
746
|
+
}
|
|
747
|
+
// Only a named site has a name worth showing: until the second screen
|
|
748
|
+
// is submitted, `siteName` is still the built-in default.
|
|
749
|
+
siteName={askSiteName ? undefined : appConfig.siteName}
|
|
750
|
+
/>
|
|
152
751
|
</BaseLayout>,
|
|
153
752
|
);
|
|
154
753
|
});
|
|
155
754
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
755
|
+
/**
|
|
756
|
+
* Open the owner's account, or pick up the one an interrupted run already
|
|
757
|
+
* opened.
|
|
758
|
+
*
|
|
759
|
+
* The resume half exists because the first screen is otherwise a place a site
|
|
760
|
+
* can brick itself: if the account is created and standing the site up then
|
|
761
|
+
* fails, the site is left `pending` with a user row in it, and from there
|
|
762
|
+
* `createAuth`'s registration hook refuses every signup — any email, not just
|
|
763
|
+
* this one — while the onboarding middleware sends `/signin` and `/reset` back
|
|
764
|
+
* to this same screen. Nothing in a browser gets out of that.
|
|
765
|
+
*
|
|
766
|
+
* So a failed signup is followed by a sign-in with the credentials just typed.
|
|
767
|
+
* It is safe because of what `pending` means: registration is closed the moment
|
|
768
|
+
* any user exists, so the only row that can be here is the one the interrupted
|
|
769
|
+
* run left, and claiming it takes that row's password. What follows is
|
|
770
|
+
* idempotent, so resuming is simply running the rest again.
|
|
771
|
+
*
|
|
772
|
+
* @param c - The setup request, for its auth instance
|
|
773
|
+
* @param credentials - The address and password the screen collected
|
|
774
|
+
* @returns The owner and the headers that sign them in, or null when neither
|
|
775
|
+
* opening nor resuming an account worked
|
|
776
|
+
*/
|
|
777
|
+
async function openOwnerSession(
|
|
778
|
+
c: Context<Env>,
|
|
779
|
+
credentials: { email: string; password: string },
|
|
780
|
+
): Promise<{ ownerUserId: string; headers: Headers } | null> {
|
|
781
|
+
const auth = c.var.auth;
|
|
782
|
+
if (!auth) return null;
|
|
783
|
+
const { email, password } = credentials;
|
|
161
784
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
785
|
+
try {
|
|
786
|
+
const { headers, response } = await auth.api.signUpEmail({
|
|
787
|
+
returnHeaders: true,
|
|
788
|
+
// A stand-in until the next screen has a site name to put here. See
|
|
789
|
+
// `deriveAccountName`.
|
|
790
|
+
body: { name: deriveAccountName(email), email, password },
|
|
791
|
+
});
|
|
792
|
+
if (response?.user?.id) {
|
|
793
|
+
return { ownerUserId: response.user.id, headers };
|
|
794
|
+
}
|
|
795
|
+
} catch (err) {
|
|
796
|
+
// Not reported yet: the next attempt is the one that decides whether this
|
|
797
|
+
// is a failure or an interrupted run being picked up.
|
|
798
|
+
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
799
|
+
console.error("Setup sign-up failed:", err);
|
|
800
|
+
}
|
|
166
801
|
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
802
|
+
try {
|
|
803
|
+
const { headers, response } = await auth.api.signInEmail({
|
|
804
|
+
returnHeaders: true,
|
|
805
|
+
body: { email, password },
|
|
806
|
+
});
|
|
807
|
+
if (response?.user?.id) {
|
|
808
|
+
return { ownerUserId: response.user.id, headers };
|
|
809
|
+
}
|
|
810
|
+
} catch {
|
|
811
|
+
// Nothing to resume, or the wrong password for what is here. Either way
|
|
812
|
+
// the caller says the same thing: check the details and try again.
|
|
178
813
|
}
|
|
179
814
|
|
|
180
|
-
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* The first screen's submission: open the owner's account and stand the site
|
|
820
|
+
* up around it.
|
|
821
|
+
*
|
|
822
|
+
* Signs the new owner in on the way out rather than sending them to `/signin`
|
|
823
|
+
* to type the password they just chose. The screen after this one is theirs to
|
|
824
|
+
* answer, and it can only be reached with a session.
|
|
825
|
+
*
|
|
826
|
+
* @param c - The setup request
|
|
827
|
+
* @param body - The submitted form
|
|
828
|
+
*/
|
|
829
|
+
async function createOwnerAccount(
|
|
830
|
+
c: Context<Env>,
|
|
831
|
+
body: Record<string, string>,
|
|
832
|
+
): Promise<Response> {
|
|
833
|
+
const i18n = getI18n(c);
|
|
181
834
|
|
|
182
835
|
if (!c.var.auth) {
|
|
183
836
|
return dsToast(
|
|
@@ -192,71 +845,166 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
192
845
|
);
|
|
193
846
|
}
|
|
194
847
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (!signUpResponse || "error" in signUpResponse) {
|
|
201
|
-
return dsToast(
|
|
202
|
-
i18n._(
|
|
203
|
-
msg({
|
|
204
|
-
message:
|
|
205
|
-
"Couldn't create your account. Check the details and try again.",
|
|
206
|
-
comment: "@context: Error toast when account creation fails",
|
|
207
|
-
}),
|
|
208
|
-
),
|
|
209
|
-
"error",
|
|
210
|
-
);
|
|
211
|
-
}
|
|
848
|
+
const parsed = SetupAccountSchema.safeParse(body);
|
|
849
|
+
if (!parsed.success) {
|
|
850
|
+
return dsToast(validationMessage(i18n, parsed.error), "error");
|
|
851
|
+
}
|
|
212
852
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
msg({
|
|
218
|
-
message:
|
|
219
|
-
"Couldn't create your account. Check the details and try again.",
|
|
220
|
-
comment: "@context: Error toast when account creation fails",
|
|
221
|
-
}),
|
|
222
|
-
),
|
|
223
|
-
"error",
|
|
224
|
-
);
|
|
225
|
-
}
|
|
853
|
+
const owner = await openOwnerSession(c, parsed.data);
|
|
854
|
+
if (!owner) {
|
|
855
|
+
return dsToast(accountCreationFailedMessage(i18n), "error");
|
|
856
|
+
}
|
|
226
857
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
? detectCjkFontFromHeader(browserLanguage)
|
|
231
|
-
: "off";
|
|
232
|
-
const siteLanguage =
|
|
233
|
-
browserLanguage && browserLanguage.trim()
|
|
234
|
-
? detectLocaleFromHeader(browserLanguage)
|
|
235
|
-
: undefined;
|
|
236
|
-
|
|
237
|
-
await c.var.services.bootstrap.completeInitialSetup({
|
|
238
|
-
ownerUserId,
|
|
239
|
-
siteName,
|
|
240
|
-
timeZone,
|
|
241
|
-
cjkSerifFont,
|
|
242
|
-
siteLanguage,
|
|
858
|
+
try {
|
|
859
|
+
await c.var.services.bootstrap.provisionOwnerAccount({
|
|
860
|
+
ownerUserId: owner.ownerUserId,
|
|
243
861
|
});
|
|
244
|
-
|
|
245
|
-
return dsRedirect(
|
|
246
|
-
toPublicPath("/signin?setup", c.var.appConfig.sitePathPrefix),
|
|
247
|
-
);
|
|
248
862
|
} catch (err) {
|
|
863
|
+
// The failure that used to be unrecoverable. The account survives it, so
|
|
864
|
+
// the next attempt resumes through `openOwnerSession` rather than trying to
|
|
865
|
+
// create it a second time.
|
|
249
866
|
// eslint-disable-next-line no-console -- Error logging is intentional
|
|
250
867
|
console.error("Setup error:", err);
|
|
251
|
-
return dsToast(
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
868
|
+
return dsToast(accountCreationFailedMessage(i18n), "error");
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Back to the same URL: the status this just wrote is what decides which
|
|
872
|
+
// screen answers, so the second step needs no address of its own.
|
|
873
|
+
return dsRedirect(toPublicPath("/setup", c.var.appConfig.sitePathPrefix), {
|
|
874
|
+
headers: owner.headers,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* The last screen's submission, for both install kinds: record what the author
|
|
880
|
+
* said about their site and let them in.
|
|
881
|
+
*
|
|
882
|
+
* The two schemas are parsed in separate branches rather than picked between,
|
|
883
|
+
* so a site name can only reach the service when the schema that requires one
|
|
884
|
+
* is the schema that ran.
|
|
885
|
+
*
|
|
886
|
+
* @param c - The setup request
|
|
887
|
+
* @param body - The submitted form
|
|
888
|
+
*/
|
|
889
|
+
async function completeSiteSetup(
|
|
890
|
+
c: Context<Env>,
|
|
891
|
+
body: Record<string, string>,
|
|
892
|
+
): Promise<Response> {
|
|
893
|
+
const i18n = getI18n(c);
|
|
894
|
+
|
|
895
|
+
// Membership is the check that makes "the owner is answering" true: a session
|
|
896
|
+
// alone can belong to someone this site has never heard of, and this branch
|
|
897
|
+
// writes the site's own name and language.
|
|
898
|
+
if (!(await isCurrentSiteMember(c))) {
|
|
899
|
+
return dsRedirect(
|
|
900
|
+
toPublicPath("/signin?redirect=/setup", c.var.appConfig.sitePathPrefix),
|
|
260
901
|
);
|
|
261
902
|
}
|
|
903
|
+
|
|
904
|
+
if (await needsSiteName(c)) {
|
|
905
|
+
const parsed = SetupSiteSchema.safeParse(body);
|
|
906
|
+
if (!parsed.success) {
|
|
907
|
+
return dsToast(validationMessage(i18n, parsed.error), "error");
|
|
908
|
+
}
|
|
909
|
+
return storeSiteAnswers(c, body, parsed.data, parsed.data.siteName);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const parsed = SetupLanguageSchema.safeParse(body);
|
|
913
|
+
if (!parsed.success) {
|
|
914
|
+
return dsToast(validationMessage(i18n, parsed.error), "error");
|
|
915
|
+
}
|
|
916
|
+
return storeSiteAnswers(c, body, parsed.data, undefined);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Write the last screen's answers and send the author into their site.
|
|
921
|
+
*
|
|
922
|
+
* @param c - The setup request
|
|
923
|
+
* @param body - The submitted form, for the two values the browser reports
|
|
924
|
+
* rather than the author choosing: its language and its time zone
|
|
925
|
+
* @param answers - The validated answers both install kinds share
|
|
926
|
+
* @param siteName - The name, on an install that was asked for one
|
|
927
|
+
*/
|
|
928
|
+
async function storeSiteAnswers(
|
|
929
|
+
c: Context<Env>,
|
|
930
|
+
body: Record<string, string>,
|
|
931
|
+
answers: SetupLanguageAnswers,
|
|
932
|
+
siteName: string | undefined,
|
|
933
|
+
): Promise<Response> {
|
|
934
|
+
const auth = c.var.auth;
|
|
935
|
+
|
|
936
|
+
await c.var.services.bootstrap.completeSiteSetup(
|
|
937
|
+
{
|
|
938
|
+
siteName,
|
|
939
|
+
siteLanguage: answers.contentLanguage,
|
|
940
|
+
browserLanguage: body.language,
|
|
941
|
+
// Left alone on a site whose clock the control plane already set: this
|
|
942
|
+
// screen can be opened from anywhere, and the browser reporting a
|
|
943
|
+
// different zone is not the author moving their site.
|
|
944
|
+
timeZone: siteName ? mapIanaToTimezone(body.timezone ?? "") : undefined,
|
|
945
|
+
},
|
|
946
|
+
{ oldLanguage: c.var.appConfig.siteLanguage },
|
|
947
|
+
{
|
|
948
|
+
// better-auth requires user.name to stay aligned with the active site
|
|
949
|
+
// display name for the current operator, the same way saving the general
|
|
950
|
+
// settings page does.
|
|
951
|
+
updateCurrentUserName: auth
|
|
952
|
+
? async (displayName) => {
|
|
953
|
+
await auth.api.updateUser({
|
|
954
|
+
body: { name: displayName },
|
|
955
|
+
headers: c.req.raw.headers,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
: null,
|
|
959
|
+
},
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
await storeDiscoverAnswer(c, answers.discover);
|
|
963
|
+
|
|
964
|
+
return dsRedirect(toPublicPath("/", c.var.appConfig.sitePathPrefix));
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
setupRoutes.post("/setup", async (c) => {
|
|
968
|
+
const status = await c.var.services.settings.getOnboardingStatus();
|
|
969
|
+
if (status === ONBOARDING_STATUS.COMPLETED)
|
|
970
|
+
return c.redirect(toPublicPath("/", c.var.appConfig.sitePathPrefix));
|
|
971
|
+
|
|
972
|
+
const body = await c.req.json<Record<string, string>>();
|
|
973
|
+
|
|
974
|
+
return status === ONBOARDING_STATUS.PENDING
|
|
975
|
+
? createOwnerAccount(c, body)
|
|
976
|
+
: completeSiteSetup(c, body);
|
|
262
977
|
});
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* The first thing a rejected setup form got wrong, in words.
|
|
981
|
+
*
|
|
982
|
+
* @param i18n - The request's catalog
|
|
983
|
+
* @param error - What the schema rejected
|
|
984
|
+
* @returns The first issue's message, or a general one when it carried none
|
|
985
|
+
*/
|
|
986
|
+
function validationMessage(
|
|
987
|
+
i18n: ReturnType<typeof getI18n>,
|
|
988
|
+
error: ZodError,
|
|
989
|
+
): string {
|
|
990
|
+
return (
|
|
991
|
+
error.issues[0]?.message ??
|
|
992
|
+
i18n._(
|
|
993
|
+
msg({
|
|
994
|
+
message: "Something doesn't look right. Check the form and try again.",
|
|
995
|
+
comment: "@context: Fallback validation error for setup form",
|
|
996
|
+
}),
|
|
997
|
+
)
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function accountCreationFailedMessage(
|
|
1002
|
+
i18n: ReturnType<typeof getI18n>,
|
|
1003
|
+
): string {
|
|
1004
|
+
return i18n._(
|
|
1005
|
+
msg({
|
|
1006
|
+
message: "Couldn't create your account. Check the details and try again.",
|
|
1007
|
+
comment: "@context: Error toast when account creation fails",
|
|
1008
|
+
}),
|
|
1009
|
+
);
|
|
1010
|
+
}
|