From 4e84e0c827efd80ab4096e9be3fbecf91be6c147 Mon Sep 17 00:00:00 2001 From: Andreas Creten Date: Wed, 3 Jul 2024 15:25:42 +0200 Subject: [PATCH] Revert "Update to latest ghost version" --- .github/scripts/docker-compose.yml | 10 - .github/workflows/ci.yml | 2 +- apps/admin-x-activitypub/package.json | 5 +- .../public/styles/reader.css | 1928 ------ apps/admin-x-activitypub/src/App.tsx | 17 +- apps/admin-x-activitypub/src/MainContent.tsx | 7 - .../src/assets/images/ap-welcome.png | Bin 81406 -> 0 bytes .../src/components/FollowSite.tsx | 85 - .../src/components/ListIndex.tsx | 292 +- .../src/components/ViewFollowers.tsx | 45 - .../src/components/ViewFollowing.tsx | 60 - .../src/components/articleBodyStyles.ts | 5911 ----------------- .../src/components/modals.tsx | 11 - apps/admin-x-activitypub/src/styles/index.css | 24 - .../src/utils/get-username.ts | 12 - .../test/acceptance/app.test.ts | 2 +- .../test/acceptance/listIndex.test.ts | 52 - .../test/unit/ListIndex.test.tsx | 10 + .../test/unit/utils/get-username.test.tsx | 36 - apps/admin-x-demo/package.json | 2 +- apps/admin-x-design-system/package.json | 29 +- .../src/global/Button.tsx | 11 +- .../src/global/ButtonGroup.stories.tsx | 25 +- .../src/global/ButtonGroup.tsx | 4 +- .../src/global/Toast.stories.tsx | 2 + .../src/global/form/TextArea.tsx | 1 + .../src/global/form/Toggle.tsx | 3 - .../src/global/layout/ViewContainer.tsx | 2 +- .../src/global/modal/Modal.tsx | 4 +- .../src/settings/SettingGroup.tsx | 50 +- .../src/settings/SettingGroupHeader.tsx | 4 +- .../admin-x-design-system/tailwind.config.cjs | 2 +- apps/admin-x-framework/package.json | 9 +- apps/admin-x-framework/src/api/actions.ts | 2 +- apps/admin-x-framework/src/api/activitypub.ts | 122 - apps/admin-x-framework/src/api/newsletters.ts | 3 +- apps/admin-x-framework/src/api/tiers.ts | 2 +- apps/admin-x-framework/src/api/users.ts | 2 +- .../src/hooks/useFilterableApi.ts | 33 +- apps/admin-x-framework/src/test/acceptance.ts | 15 +- .../test/responses/activitypub/following.json | 13 - .../src/test/responses/activitypub/inbox.json | 155 - .../src/test/responses/newsletters.json | 1 - .../src/utils/api/fetchApi.ts | 11 +- apps/admin-x-framework/src/utils/api/hooks.ts | 15 +- .../src/utils/api/updateQueries.ts | 2 +- apps/admin-x-framework/src/utils/errors.ts | 8 +- apps/admin-x-framework/src/utils/helpers.ts | 4 +- .../src/utils/queryClient.ts | 3 +- .../test/unit/utils/api/hooks.test.tsx | 35 - apps/admin-x-settings/package.json | 10 +- apps/admin-x-settings/src/MainContent.tsx | 2 +- .../settings/advanced/CodeInjection.tsx | 2 +- .../components/settings/advanced/History.tsx | 2 +- .../settings/advanced/Integrations.tsx | 2 +- .../src/components/settings/advanced/Labs.tsx | 4 +- .../advanced/integrations/AmpModal.tsx | 26 +- .../integrations/CustomIntegrationModal.tsx | 4 +- .../integrations/FirstPromoterModal.tsx | 28 +- .../advanced/integrations/PinturaModal.tsx | 42 +- .../advanced/integrations/SlackModal.tsx | 20 +- .../advanced/integrations/UnsplashModal.tsx | 42 +- .../settings/advanced/labs/AlphaFeatures.tsx | 16 +- .../settings/advanced/labs/BetaFeatures.tsx | 6 +- .../settings/advanced/labs/FeatureToggle.tsx | 4 +- .../settings/email/DefaultRecipients.tsx | 2 +- .../components/settings/email/Newsletters.tsx | 2 +- .../newsletters/NewsletterDetailModal.tsx | 64 +- .../email/newsletters/NewsletterPreview.tsx | 1 - .../newsletters/NewsletterPreviewContent.tsx | 71 +- .../email/useDefaultRecipientsOptions.tsx | 10 +- .../settings/general/SocialAccounts.tsx | 2 +- .../settings/general/UserDetailModal.tsx | 7 +- .../src/components/settings/general/Users.tsx | 2 +- .../settings/general/users/ProfileBasics.tsx | 2 +- .../src/components/settings/growth/Offers.tsx | 2 +- .../settings/growth/Recommendations.tsx | 4 +- .../growth/embedSignup/EmbedSignupForm.tsx | 2 +- .../growth/embedSignup/EmbedSignupSidebar.tsx | 1 + .../EditRecommendationModal.tsx | 8 +- .../RecommendationDescriptionForm.tsx | 1 + .../components/settings/membership/Portal.tsx | 2 +- .../membership/stripe/StripeConnectModal.tsx | 2 +- .../membership/tiers/TierDetailModal.tsx | 10 +- .../settings/site/AnnouncementBar.tsx | 2 +- .../settings/site/DesignSetting.tsx | 2 +- .../components/settings/site/Navigation.tsx | 2 +- .../components/settings/site/ThemeModal.tsx | 17 +- .../site/theme/AdvancedThemeSettings.tsx | 23 +- apps/admin-x-settings/src/utils/socialUrls.ts | 16 +- .../advanced/integrations/amp.test.ts | 2 +- .../advanced/integrations/custom.test.ts | 5 +- .../integrations/firstPromoter.test.ts | 2 +- .../advanced/integrations/slack.test.ts | 9 +- .../advanced/integrations/unsplash.test.ts | 2 - .../email/defaultRecipients.test.ts | 29 - .../acceptance/general/socialAccounts.test.ts | 14 +- .../acceptance/general/users/profile.test.ts | 28 +- .../acceptance/general/users/roles.test.ts | 5 +- .../test/acceptance/membership/tiers.test.ts | 13 +- .../test/acceptance/site/theme.test.ts | 38 - .../test/utils/responses/source.zip | Bin 4195 -> 0 bytes apps/announcement-bar/package.json | 1 - apps/comments-ui/package.json | 8 +- .../src/components/content/CTABox.tsx | 4 +- apps/comments-ui/src/utils/api.ts | 2 +- apps/comments-ui/test/e2e/pagination.test.ts | 48 +- apps/comments-ui/test/utils/e2e.ts | 8 - apps/portal/package.json | 8 +- apps/portal/src/utils/fixtures-generator.js | 9 +- apps/signup-form/package.json | 25 +- apps/sodo-search/package.json | 1 - ghost/admin/.lint-todo | 5 - .../dashboard/resources/whats-new.hbs | 8 +- .../editor/modals/publish-flow/complete.hbs | 13 +- .../components/editor/publish-management.js | 10 +- ghost/admin/app/components/gh-alert.js | 4 +- .../components/gh-editor-feature-image.hbs | 4 +- .../components/gh-koenig-editor-lexical.hbs | 34 +- .../components/gh-koenig-editor-lexical.js | 111 +- .../components/gh-member-settings-form.hbs | 16 +- .../app/components/gh-member-settings-form.js | 54 +- .../components/gh-nav-menu/footer-banner.hbs | 28 - .../components/gh-nav-menu/footer-banner.js | 89 - .../app/components/gh-nav-menu/footer.hbs | 23 +- .../app/components/gh-nav-menu/footer.js | 8 +- .../admin/app/components/gh-nav-menu/main.hbs | 2 +- .../admin/app/components/gh-nav-menu/main.js | 2 +- .../app/components/gh-post-settings-menu.hbs | 3 +- .../components/gh-token-input/label-token.js | 9 +- .../app/components/koenig-image-editor.hbs | 4 +- .../app/components/koenig-lexical-editor.js | 66 +- .../members-activity/event-type-filter.js | 3 +- ghost/admin/app/components/members/filter.js | 18 +- .../admin/app/components/modal-member-tier.js | 13 +- .../app/components/modal-post-history.hbs | 40 +- .../app/components/modal-post-history.js | 46 +- .../modals/editor/confirm-leave.hbs | 6 +- .../components/modals/restore-revision.hbs | 3 +- .../app/components/modals/restore-revision.js | 8 - .../admin/app/components/modals/whats-new.hbs | 24 - .../admin/app/components/modals/whats-new.js | 15 - .../app/components/posts-list/list-item.hbs | 6 +- ghost/admin/app/components/posts/analytics.js | 24 +- .../app/components/posts/old-analytics.js | 28 +- ghost/admin/app/controllers/lexical-editor.js | 201 +- ghost/admin/app/controllers/member.js | 1 - ghost/admin/app/controllers/members.js | 18 +- ghost/admin/app/controllers/offer.js | 13 +- ghost/admin/app/controllers/reset.js | 5 +- ghost/admin/app/controllers/setup/done.js | 72 + ghost/admin/app/controllers/signin.js | 9 +- ghost/admin/app/helpers/parse-member-event.js | 15 +- ghost/admin/app/models/newsletter.js | 1 - ghost/admin/app/models/post-revision.js | 1 - ghost/admin/app/routes/application.js | 45 +- ghost/admin/app/routes/collection.js | 2 - ghost/admin/app/routes/member.js | 2 - ghost/admin/app/routes/members.js | 13 +- ghost/admin/app/routes/setup/done.js | 17 +- ghost/admin/app/routes/setup/index.js | 5 +- ghost/admin/app/routes/tag.js | 2 - ghost/admin/app/serializers/post-revision.js | 11 +- ghost/admin/app/services/dashboard-stats.js | 44 +- ghost/admin/app/services/feature.js | 4 +- ghost/admin/app/services/onboarding.js | 4 +- .../app/services/search-provider-beta.js | 139 - ghost/admin/app/services/search-provider.js | 116 - ghost/admin/app/services/search.js | 112 +- ghost/admin/app/services/whats-new.js | 51 +- ghost/admin/app/styles/app-dark.css | 10 - ghost/admin/app/styles/app.css | 1 - .../app/styles/components/notifications.css | 4 +- ghost/admin/app/styles/layouts/editor.css | 221 +- ghost/admin/app/styles/layouts/flow.css | 13 +- ghost/admin/app/styles/layouts/main.css | 141 +- .../admin/app/styles/layouts/post-history.css | 4 - .../app/styles/layouts/whatsnew-modal.css | 150 - ghost/admin/app/styles/layouts/whatsnew.css | 2 +- ghost/admin/app/styles/patterns/forms.css | 4 +- .../app/styles/spirit/_custom-styles.css | 6 +- ghost/admin/app/templates/dashboard.hbs | 52 +- ghost/admin/app/templates/lexical-editor.hbs | 7 +- ghost/admin/app/templates/setup/done.hbs | 46 + ghost/admin/app/templates/signin.hbs | 2 +- ghost/admin/app/utils/merge-stats-by-date.js | 26 - ghost/admin/app/utils/sentry.js | 9 +- ghost/admin/app/utils/subscription-data.js | 98 - ghost/admin/app/validators/post.js | 6 +- ghost/admin/mirage/config/members.js | 2 +- .../mirage/factories/member-activity-event.js | 4 +- ghost/admin/mirage/factories/member.js | 2 +- ghost/admin/mirage/factories/offer.js | 2 +- ghost/admin/mirage/factories/page.js | 54 - ghost/admin/mirage/factories/post-revision.js | 5 - ghost/admin/mirage/models/page.js | 2 +- ghost/admin/mirage/models/post-revision.js | 6 - ghost/admin/mirage/models/post.js | 3 +- ghost/admin/mirage/models/user.js | 3 +- .../admin/mirage/serializers/post-revision.js | 9 - ghost/admin/mirage/serializers/post.js | 22 +- ghost/admin/package.json | 25 +- .../assets/icons/event-email-changed.svg | 4 - .../filter-dropdown-email-address-changed.svg | 4 - .../public/assets/icons/koenig/kg-edit.svg | 6 + .../public/assets/icons/koenig/kg-trash.svg | 5 +- .../public/assets/icons/koenig/kg-wand.svg | 3 - .../public/assets/icons/sparkle-fill.svg | 1 - .../public/assets/icons/warning-fill.svg | 1 - ghost/admin/tests/acceptance/editor-test.js | 67 - .../acceptance/editor/post-revisions-test.js | 193 - .../acceptance/editor/publish-flow-test.js | 83 +- .../admin/tests/acceptance/onboarding-test.js | 15 +- .../tests/acceptance/password-reset-test.js | 6 +- ghost/admin/tests/acceptance/search-test.js | 287 +- ghost/admin/tests/acceptance/setup-test.js | 6 +- .../integration/components/gh-alert-test.js | 4 +- .../tests/integration/services/search-test.js | 61 +- .../components/koenig-lexical-editor-test.js | 58 +- .../tests/unit/controllers/editor-test.js | 182 +- .../unit/utils/merge-stats-by-date-test.js | 184 - .../unit/utils/subscription-data-test.js | 341 - ghost/api-framework/lib/utils/options.js | 9 - ghost/api-framework/package.json | 8 +- ghost/api-framework/test/util/options.test.js | 8 - ghost/audience-feedback/package.json | 2 +- ghost/bookshelf-repository/package.json | 2 +- ghost/bootstrap-socket/package.json | 2 +- ghost/collections/package.json | 6 +- ghost/core/core/boot.js | 13 +- ghost/core/core/frontend/helpers/get.js | 12 +- .../core/core/frontend/helpers/ghost_head.js | 25 +- .../core/frontend/src/cards/css/bookmark.css | 2 - ghost/core/core/frontend/utils/images.js | 4 - .../web/middleware/frontend-caching.js | 77 - .../core/frontend/web/middleware/index.js | 1 - ghost/core/core/frontend/web/site.js | 63 +- ghost/core/core/server/GhostServer.js | 4 +- .../utils/serializers/input/pages.js | 44 +- .../utils/serializers/input/posts.js | 52 +- .../serializers/output/authentication.js | 2 +- ghost/core/core/server/data/db/index.js | 9 + ...-55-add-show-subhead-column-newsletters.js | 9 - ...9-13-33-rename-newsletters-show-subhead.js | 3 - ...37-add-custom-excerpt-to-post-revisions.js | 7 - ...-populate-post-revisions-custom-excerpt.js | 11 - ...-48-35-rename-newsletters-show-subtitle.js | 3 - ...-10-14-53-31-add-posts-updated-at-index.js | 4 - ...-12-08-20-add-posts-tags-post-tag-index.js | 26 - ...-add-posts-type-status-updated-at-index.js | 4 - ghost/core/core/server/data/schema/schema.js | 14 +- .../core/server/models/base/plugins/crud.js | 5 - ghost/core/core/server/models/newsletter.js | 3 +- ghost/core/core/server/models/post.js | 3 +- .../services/custom-redirects/validation.js | 24 +- .../server/services/members-events/index.js | 4 +- .../server/services/members/middleware.js | 76 - ghost/core/core/server/services/slack.js | 24 +- .../services/webhooks/WebhookTrigger.js | 3 +- .../server/web/api/endpoints/admin/app.js | 2 +- .../core/server/web/api/middleware/upload.js | 39 +- ghost/core/core/server/web/members/app.js | 9 +- ghost/core/core/server/web/parent/app.js | 2 +- ghost/core/core/shared/config/defaults.json | 2 +- ghost/core/core/shared/instrumentation.js | 34 - ghost/core/core/shared/labs.js | 10 +- ghost/core/core/shared/sentry.js | 1 + ghost/core/package.json | 79 +- .../__snapshots__/email-previews.test.js.snap | 92 +- .../admin/__snapshots__/members.test.js.snap | 192 +- .../__snapshots__/newsletters.test.js.snap | 161 +- .../admin/__snapshots__/settings.test.js.snap | 2 +- ghost/core/test/e2e-api/admin/members.test.js | 77 - .../content/__snapshots__/posts.test.js.snap | 188 +- ghost/core/test/e2e-api/content/posts.test.js | 44 +- .../__snapshots__/middleware.test.js.snap | 156 - .../test/e2e-api/members/middleware.test.js | 62 +- .../core/test/e2e-browser/admin/tiers.spec.js | 9 +- .../e2e-browser/utils/e2e-browser-utils.js | 8 +- ghost/core/test/e2e-frontend/members.test.js | 7 +- .../core/test/e2e-frontend/middleware.test.js | 94 - .../__snapshots__/click-tracking.test.js.snap | 43 - .../test/e2e-server/click-tracking.test.js | 46 +- .../__snapshots__/batch-sending.test.js.snap | 264 +- .../__snapshots__/cards.test.js.snap | 62 +- .../services/email-service/cards.test.js | 6 +- .../__snapshots__/authentication.test.js.snap | 4 +- .../utils/serializers/input/pages.test.js | 60 - .../utils/serializers/input/posts.test.js | 60 - .../__snapshots__/ghost_head.test.js.snap | 13 - .../unit/frontend/helpers/ghost_head.test.js | 24 - .../frontend/meta/image-dimensions.test.js | 45 - .../web/middleware/frontend-caching.test.js | 94 - .../unit/server/data/schema/integrity.test.js | 2 +- .../custom-redirects/validation.test.js | 20 - .../test/unit/server/services/slack.test.js | 24 +- .../server/services/webhooks/trigger.test.js | 180 +- .../server/web/api/middleware/upload.test.js | 45 +- .../utils/fixtures/images/svg-malformed.svg | 1 - .../utils/fixtures/images/svg-with-script.svg | 8 - .../fixtures/images/svg-with-script2.svg | 6 - ghost/core/test/utils/stripe-mocker.js | 81 +- .../package.json | 4 +- .../lib/importers/CommentsImporter.js | 73 - .../lib/importers/PostsImporter.js | 48 +- .../RecommendationClickEventsImporter.js | 2 +- ghost/data-generator/lib/importers/index.js | 3 +- ghost/data-generator/package.json | 6 +- ghost/domain-events/package.json | 2 +- ghost/email-addresses/package.json | 2 +- ghost/email-analytics-service/package.json | 2 +- ghost/email-service/lib/EmailRenderer.js | 92 +- .../email-templates/partials/styles-old.hbs | 195 +- .../lib/email-templates/partials/styles.hbs | 139 +- .../lib/email-templates/template-old.hbs | 17 +- .../lib/email-templates/template.hbs | 2 +- .../lib/helpers/register-helpers.js | 55 - ghost/email-service/package.json | 10 +- .../email-service/test/email-helpers.test.js | 142 - .../email-service/test/email-renderer.test.js | 112 +- ghost/ghost/package.json | 18 +- .../core/activitypub/activity.entity.test.ts | 89 + .../src/core/activitypub/activity.entity.ts | 127 + .../src/core/activitypub/activity.event.ts | 14 + .../core/activitypub/activity.repository.ts | 6 + .../core/activitypub/activity.service.test.ts | 145 + .../src/core/activitypub/activity.service.ts | 36 + .../activitypub/activitypub.service.test.ts | 61 + .../core/activitypub/activitypub.service.ts | 29 + .../src/core/activitypub/actor.entity.test.ts | 229 + .../src/core/activitypub/actor.entity.ts | 262 + .../src/core/activitypub/actor.repository.ts | 8 + .../src/core/activitypub/article.object.ts | 63 + .../http-signature.service.test.ts | 100 + .../activitypub/http-signature.service.ts | 209 + .../core/activitypub/inbox.service.test.ts | 83 + .../src/core/activitypub/inbox.service.ts | 25 + .../core/activitypub/jsonld.service.test.ts | 122 + .../src/core/activitypub/jsonld.service.ts | 91 + .../src/core/activitypub/post.repository.ts | 19 + .../tell-the-world.service.test.ts | 116 + .../activitypub/tell-the-world.service.ts | 86 + ghost/ghost/src/core/activitypub/types.ts | 79 + .../ghost/src/core/activitypub/uri.object.ts | 12 + .../activitypub/webfinger.service.test.ts | 79 + .../src/core/activitypub/webfinger.service.ts | 81 + .../activity.repository.in-memory.ts | 18 + .../in-memory/actor.repository.in-memory.ts | 63 + .../ghost/src/db/knex/post.repository.knex.ts | 58 + .../activitypub.controller.test.ts | 20 + .../controllers/activitypub.controller.ts | 28 + .../activitypub.controller.test.ts | 142 + .../controllers/activitypub.controller.ts | 108 + .../controllers/webfinger.controller.test.ts | 38 + .../controllers/webfinger.controller.ts | 18 + .../src/listeners/activity.listener.test.ts | 51 + .../ghost/src/listeners/activity.listener.ts | 15 + .../src/nestjs/modules/activitypub.module.ts | 47 + .../src/nestjs/modules/admin-api.module.ts | 12 +- ghost/ghost/src/nestjs/modules/app.module.ts | 7 +- ghost/i18n/lib/i18n.js | 1 - ghost/i18n/locales/bs/comments.json | 73 - ghost/i18n/locales/bs/ghost.json | 34 - ghost/i18n/locales/bs/portal.json | 159 - ghost/i18n/locales/bs/signup-form.json | 9 - ghost/i18n/locales/el/comments.json | 73 - ghost/i18n/locales/el/ghost.json | 34 - ghost/i18n/locales/el/portal.json | 159 - ghost/i18n/locales/el/signup-form.json | 9 - ghost/i18n/locales/sk/portal.json | 40 +- ghost/i18n/package.json | 4 +- ghost/importer-revue/package.json | 4 +- ghost/job-manager/package.json | 2 +- ghost/link-redirects/README.md | 130 - ghost/link-replacer/package.json | 4 +- .../lib/LinkClickTrackingService.js | 3 + ghost/link-tracking/package.json | 2 +- ghost/magic-link/package.json | 4 +- ghost/mail-events/package.json | 2 +- ghost/mailgun-client/package.json | 6 +- .../lib/controllers/RouterController.js | 27 +- ghost/members-api/lib/members-api.js | 3 +- .../lib/repositories/EventRepository.js | 39 +- .../lib/repositories/MemberRepository.js | 11 +- .../lib/services/MemberBREADService.js | 3 - ghost/members-api/package.json | 6 +- .../test/unit/lib/controllers/router.test.js | 73 - .../lib/LastSeenAtUpdater.js | 25 +- ghost/members-events-service/package.json | 2 +- .../test/last-seen-at-updater.test.js | 190 +- ghost/members-importer/package.json | 6 +- ghost/members-ssr/lib/members-ssr.js | 5 - ghost/members-ssr/package.json | 2 +- ghost/minifier/package.json | 6 +- ghost/mw-error-handler/package.json | 6 +- ghost/mw-version-match/package.json | 2 +- ghost/oembed-service/lib/OEmbedService.js | 18 +- ghost/oembed-service/package.json | 20 +- .../test/oembed-service.test.js | 53 +- ghost/offers/package.json | 2 +- ghost/package-json/package.json | 2 +- ghost/post-revisions/src/PostRevisions.ts | 6 +- .../post-revisions/test/PostRevisions.test.ts | 22 - ghost/posts-service/lib/PostsService.js | 9 + ghost/posts-service/package.json | 2 +- ghost/recommendations/package.json | 6 +- ghost/settings-path-manager/package.json | 2 +- ghost/slack-notifications/package.json | 4 +- ghost/stripe/package.json | 4 +- ghost/tiers/package.json | 2 +- ghost/update-check-service/package.json | 6 +- ghost/webmentions/package.json | 2 +- package.json | 4 +- yarn.lock | 4063 +++++------ 414 files changed, 6745 insertions(+), 19179 deletions(-) delete mode 100644 apps/admin-x-activitypub/public/styles/reader.css delete mode 100644 apps/admin-x-activitypub/src/MainContent.tsx delete mode 100644 apps/admin-x-activitypub/src/assets/images/ap-welcome.png delete mode 100644 apps/admin-x-activitypub/src/components/FollowSite.tsx delete mode 100644 apps/admin-x-activitypub/src/components/ViewFollowers.tsx delete mode 100644 apps/admin-x-activitypub/src/components/ViewFollowing.tsx delete mode 100644 apps/admin-x-activitypub/src/components/articleBodyStyles.ts delete mode 100644 apps/admin-x-activitypub/src/components/modals.tsx delete mode 100644 apps/admin-x-activitypub/src/utils/get-username.ts delete mode 100644 apps/admin-x-activitypub/test/acceptance/listIndex.test.ts create mode 100644 apps/admin-x-activitypub/test/unit/ListIndex.test.tsx delete mode 100644 apps/admin-x-activitypub/test/unit/utils/get-username.test.tsx delete mode 100644 apps/admin-x-framework/src/api/activitypub.ts delete mode 100644 apps/admin-x-framework/src/test/responses/activitypub/following.json delete mode 100644 apps/admin-x-framework/src/test/responses/activitypub/inbox.json delete mode 100644 apps/admin-x-settings/test/utils/responses/source.zip delete mode 100644 ghost/admin/app/components/gh-nav-menu/footer-banner.hbs delete mode 100644 ghost/admin/app/components/gh-nav-menu/footer-banner.js delete mode 100644 ghost/admin/app/components/modals/whats-new.hbs delete mode 100644 ghost/admin/app/components/modals/whats-new.js create mode 100644 ghost/admin/app/controllers/setup/done.js delete mode 100644 ghost/admin/app/services/search-provider-beta.js delete mode 100644 ghost/admin/app/services/search-provider.js delete mode 100644 ghost/admin/app/styles/layouts/whatsnew-modal.css create mode 100644 ghost/admin/app/templates/setup/done.hbs delete mode 100644 ghost/admin/app/utils/merge-stats-by-date.js delete mode 100644 ghost/admin/app/utils/subscription-data.js delete mode 100644 ghost/admin/mirage/factories/page.js delete mode 100644 ghost/admin/mirage/factories/post-revision.js delete mode 100644 ghost/admin/mirage/models/post-revision.js delete mode 100644 ghost/admin/mirage/serializers/post-revision.js delete mode 100644 ghost/admin/public/assets/icons/event-email-changed.svg delete mode 100644 ghost/admin/public/assets/icons/filter-dropdown-email-address-changed.svg create mode 100644 ghost/admin/public/assets/icons/koenig/kg-edit.svg delete mode 100644 ghost/admin/public/assets/icons/koenig/kg-wand.svg delete mode 100644 ghost/admin/public/assets/icons/sparkle-fill.svg delete mode 100644 ghost/admin/public/assets/icons/warning-fill.svg delete mode 100644 ghost/admin/tests/acceptance/editor/post-revisions-test.js delete mode 100644 ghost/admin/tests/unit/utils/merge-stats-by-date-test.js delete mode 100644 ghost/admin/tests/unit/utils/subscription-data-test.js delete mode 100644 ghost/core/core/frontend/web/middleware/frontend-caching.js delete mode 100644 ghost/core/core/server/data/migrations/versions/5.83/2024-05-28-02-20-55-add-show-subhead-column-newsletters.js delete mode 100644 ghost/core/core/server/data/migrations/versions/5.84/2024-06-04-09-13-33-rename-newsletters-show-subhead.js delete mode 100644 ghost/core/core/server/data/migrations/versions/5.84/2024-06-04-11-10-37-add-custom-excerpt-to-post-revisions.js delete mode 100644 ghost/core/core/server/data/migrations/versions/5.84/2024-06-05-08-42-34-populate-post-revisions-custom-excerpt.js delete mode 100644 ghost/core/core/server/data/migrations/versions/5.84/2024-06-05-13-48-35-rename-newsletters-show-subtitle.js delete mode 100644 ghost/core/core/server/data/migrations/versions/5.85/2024-06-10-14-53-31-add-posts-updated-at-index.js delete mode 100644 ghost/core/core/server/data/migrations/versions/5.87/2024-06-25-12-08-20-add-posts-tags-post-tag-index.js delete mode 100644 ghost/core/core/server/data/migrations/versions/5.87/2024-06-25-12-08-45-add-posts-type-status-updated-at-index.js delete mode 100644 ghost/core/core/shared/instrumentation.js delete mode 100644 ghost/core/test/e2e-frontend/middleware.test.js delete mode 100644 ghost/core/test/e2e-server/__snapshots__/click-tracking.test.js.snap delete mode 100644 ghost/core/test/unit/frontend/web/middleware/frontend-caching.test.js delete mode 100644 ghost/core/test/utils/fixtures/images/svg-malformed.svg delete mode 100644 ghost/core/test/utils/fixtures/images/svg-with-script.svg delete mode 100644 ghost/core/test/utils/fixtures/images/svg-with-script2.svg delete mode 100644 ghost/data-generator/lib/importers/CommentsImporter.js delete mode 100644 ghost/email-service/lib/helpers/register-helpers.js delete mode 100644 ghost/email-service/test/email-helpers.test.js create mode 100644 ghost/ghost/src/core/activitypub/activity.entity.test.ts create mode 100644 ghost/ghost/src/core/activitypub/activity.entity.ts create mode 100644 ghost/ghost/src/core/activitypub/activity.event.ts create mode 100644 ghost/ghost/src/core/activitypub/activity.repository.ts create mode 100644 ghost/ghost/src/core/activitypub/activity.service.test.ts create mode 100644 ghost/ghost/src/core/activitypub/activity.service.ts create mode 100644 ghost/ghost/src/core/activitypub/activitypub.service.test.ts create mode 100644 ghost/ghost/src/core/activitypub/activitypub.service.ts create mode 100644 ghost/ghost/src/core/activitypub/actor.entity.test.ts create mode 100644 ghost/ghost/src/core/activitypub/actor.entity.ts create mode 100644 ghost/ghost/src/core/activitypub/actor.repository.ts create mode 100644 ghost/ghost/src/core/activitypub/article.object.ts create mode 100644 ghost/ghost/src/core/activitypub/http-signature.service.test.ts create mode 100644 ghost/ghost/src/core/activitypub/http-signature.service.ts create mode 100644 ghost/ghost/src/core/activitypub/inbox.service.test.ts create mode 100644 ghost/ghost/src/core/activitypub/inbox.service.ts create mode 100644 ghost/ghost/src/core/activitypub/jsonld.service.test.ts create mode 100644 ghost/ghost/src/core/activitypub/jsonld.service.ts create mode 100644 ghost/ghost/src/core/activitypub/post.repository.ts create mode 100644 ghost/ghost/src/core/activitypub/tell-the-world.service.test.ts create mode 100644 ghost/ghost/src/core/activitypub/tell-the-world.service.ts create mode 100644 ghost/ghost/src/core/activitypub/types.ts create mode 100644 ghost/ghost/src/core/activitypub/uri.object.ts create mode 100644 ghost/ghost/src/core/activitypub/webfinger.service.test.ts create mode 100644 ghost/ghost/src/core/activitypub/webfinger.service.ts create mode 100644 ghost/ghost/src/db/in-memory/activity.repository.in-memory.ts create mode 100644 ghost/ghost/src/db/in-memory/actor.repository.in-memory.ts create mode 100644 ghost/ghost/src/db/knex/post.repository.knex.ts create mode 100644 ghost/ghost/src/http/admin/controllers/activitypub.controller.test.ts create mode 100644 ghost/ghost/src/http/admin/controllers/activitypub.controller.ts create mode 100644 ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts create mode 100644 ghost/ghost/src/http/frontend/controllers/activitypub.controller.ts create mode 100644 ghost/ghost/src/http/frontend/controllers/webfinger.controller.test.ts create mode 100644 ghost/ghost/src/http/frontend/controllers/webfinger.controller.ts create mode 100644 ghost/ghost/src/listeners/activity.listener.test.ts create mode 100644 ghost/ghost/src/listeners/activity.listener.ts create mode 100644 ghost/ghost/src/nestjs/modules/activitypub.module.ts delete mode 100644 ghost/i18n/locales/bs/comments.json delete mode 100644 ghost/i18n/locales/bs/ghost.json delete mode 100644 ghost/i18n/locales/bs/portal.json delete mode 100644 ghost/i18n/locales/bs/signup-form.json delete mode 100644 ghost/i18n/locales/el/comments.json delete mode 100644 ghost/i18n/locales/el/ghost.json delete mode 100644 ghost/i18n/locales/el/portal.json delete mode 100644 ghost/i18n/locales/el/signup-form.json diff --git a/.github/scripts/docker-compose.yml b/.github/scripts/docker-compose.yml index a8d89941f7d..b8f14042ced 100644 --- a/.github/scripts/docker-compose.yml +++ b/.github/scripts/docker-compose.yml @@ -27,13 +27,3 @@ services: ports: - "6379:6379" restart: always - jaeger: - image: jaegertracing/all-in-one:1.58 - container_name: ghost-jaeger - ports: - - "4318:4318" - - "16686:16686" - - "9411:9411" - restart: always - environment: - COLLECTOR_ZIPKIN_HOST_PORT: :9411 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 827e3931e1c..3c77275c57d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -402,7 +402,7 @@ jobs: jq '[{ name: "Boot time", unit: "s", value: .results[0].median, range: ((.results[0].max - .results[0].min) | tostring) }]' < boot-perf.json > boot-perf-formatted.json - name: Run analysis - uses: benchmark-action/github-action-benchmark@v1.20.3 + uses: benchmark-action/github-action-benchmark@v1.20.1 with: tool: 'customSmallerIsBetter' output-file-path: ghost/core/boot-perf-formatted.json diff --git a/apps/admin-x-activitypub/package.json b/apps/admin-x-activitypub/package.json index 8431203950e..82fcadfb30d 100644 --- a/apps/admin-x-activitypub/package.json +++ b/apps/admin-x-activitypub/package.json @@ -25,18 +25,17 @@ "lint": "yarn run lint:code && yarn run lint:test", "lint:code": "eslint --ext .js,.ts,.cjs,.tsx --cache src", "lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx --cache test", - "test:unit": "yarn nx build && vitest run", + "test:unit": "vitest run", "test:acceptance": "NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' VITE_TEST=true playwright test", "test:acceptance:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=100 yarn test:acceptance --headed", "test:acceptance:full": "ALL_BROWSERS=1 yarn test:acceptance", "preview": "vite preview" }, "devDependencies": { - "@playwright/test": "1.38.1", "@testing-library/react": "14.1.0", "@tryghost/admin-x-design-system": "0.0.0", "@tryghost/admin-x-framework": "0.0.0", - "@types/react": "18.3.3", + "@types/react": "18.3.2", "@types/react-dom": "18.3.0", "react": "18.3.1", "react-dom": "18.3.1" diff --git a/apps/admin-x-activitypub/public/styles/reader.css b/apps/admin-x-activitypub/public/styles/reader.css deleted file mode 100644 index 4cdaee420e1..00000000000 --- a/apps/admin-x-activitypub/public/styles/reader.css +++ /dev/null @@ -1,1928 +0,0 @@ - -.gh-whats-new-canvas .gh-canvas-header-content { - margin-bottom: -1px; - padding: 8px 0 16px; - align-items: center; -} - -.gh-whats-new { - flex-grow: 2; - color: var(--darkgrey); - font-size: 1.5rem; - letter-spacing: 0; - margin-top: -24px; -} - -.gh-whats-new-heading { - display: flex; - align-items: center; - font-size: 1.5rem; - letter-spacing: 0; - line-height: 1.3em; - font-weight: 700; - margin: 0; -} - -.gh-whats-new-heading svg { - width: 20px; - height: 20px; - margin-top: -2px; - margin-right: 12px; -} - -.gh-whats-new-heading svg path { - fill: var(--pink); -} - -.gh-wn-header { - position: relative; - display: flex; - align-items: center; - margin: -32px -32px 0; - padding: 18px 18px 12px; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - overflow: hidden; - background-position: center; - background-repeat: no-repeat; - background-size: cover; - background: var(--pink); - background: linear-gradient(135deg, color-mod(var(--pink) h(-10) s(+5%) l(-10%)) 0%, rgba(173,38,180,1) 100%); -} - -.gh-wn-header .background-img { - position: absolute; - top: -30px; - left: 0; -} - -.gh-wn-header h2 { - font-size: 1.3rem; - font-weight: 600; - text-transform: uppercase; - color: #FFF; - margin: 0 8px 4px; -} - -.gh-wn-header svg path { - fill: #fff; -} - -.gh-wn-close { - stroke: #FFF; - opacity: 0.6; - transition: all 0.2s ease-in-out; -} - -.gh-wn-close:hover { - opacity: 1.0; -} - -.gh-wn-entry { - margin: 0 0 5vmin; - padding-bottom: 5vmin; - width: 100%; - border-bottom: 1px solid var(--lightgrey-l2); - color: inherit; - text-decoration: none; -} - -.gh-wn-content { - max-width: 620px; -} - -.gh-whats-new-canvas .gh-wn-content { - margin: 0 auto; -} - -.gh-wn-entry h4 { - font-size: 1.2rem; - font-weight: 500; - letter-spacing: 0; - text-transform: uppercase; - margin: 24px 0 4px; - color: var(--midlightgrey); -} - -.gh-wn-entry h1 { - font-size: 3.7rem; - line-height: 1.3em; - font-weight: 700; - letter-spacing: -0.021em; - color: var(--black); - margin-bottom: 16px; -} - -.gh-whats-new-canvas .gh-wn-entry h1, -.gh-whats-new-canvas .gh-wn-entry h4 { - max-width: 620px; - margin-left: auto; - margin-right: auto; -} - -.gh-wn-entry h2 { - border-bottom: none; - font-size: 1.9rem; - padding-bottom: 0; - margin-bottom: 20px; -} - -.gh-wn-entry p, -.gh-wn-entry li { - line-height: 1.6em; -} - -.gh-wn-entry li { - margin-bottom: 12px; -} - -.gh-wn-entry p { - margin: 0 0 20px; - padding: 0; -} - -.gh-wn-entry figure { - margin-bottom: 24px; - overflow: hidden; -} - -.gh-wn-entry img { - height: auto; -} - -.gh-wn-entry hr { - border-top: 1px solid var(--whitegrey-l1); - margin: 24px 0; -} - - -/* Bookmark card details */ -.gh-wn-entry .kg-bookmark-card { - margin-bottom: 20px; -} - -.gh-wn-entry .kg-bookmark-container { - display: flex; - font-family: var(--font-family); - color: var(--darkgrey); - text-decoration: none; - min-height: 148px; - box-shadow: 0px 2px 5px -1px rgba(0, 0, 0, 0.15), 0 0 1px rgba(0, 0, 0, 0.09); - border-radius: 3px; -} - -.gh-wn-entry .kg-bookmark-content { - display: flex; - flex-direction: column; - flex-grow: 1; - align-items: flex-start; - justify-content: flex-start; - padding: 16px; -} - -.gh-wn-entry .kg-bookmark-title { - font-size: 1.3rem; - line-height: 1.5em; - font-weight: 600; - color: color(var(--midgrey) l(-30%)); -} - -.gh-wn-entry .kg-bookmark-container:hover .kg-bookmark-title { - color: var(--blue); -} - -.gh-wn-entry .kg-bookmark-description { - display: -webkit-box; - font-size: 1.25rem; - line-height: 1.5em; - color: color(var(--midgrey) l(-10%)); - font-weight: 400; - margin-top: 12px; - max-height: 36px; - overflow-y: hidden; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} - -.gh-wn-entry .kg-bookmark-thumbnail { - position: relative; - min-width: 40%; - max-height: 100%; -} - -.gh-wn-entry .kg-bookmark-thumbnail img { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 0 3px 3px 0; -} - -.gh-wn-entry .kg-bookmark-metadata { - display: flex; - align-items: center; - font-size: 1.25rem; - font-weight: 400; - color: color(var(--midgrey) l(-10%)); - margin-top: 14px; - flex-wrap: wrap; -} - -.gh-wn-entry .kg-bookmark-icon { - width: 18px; - height: 18px; - margin-right: 8px; -} - -.gh-wn-entry .kg-bookmark-author { - line-height: 1.5em; -} - -.gh-wn-entry .kg-bookmark-author:after { - content: "•"; - margin: 0 6px; -} - -.gh-wn-entry .kg-bookmark-publisher { - overflow: hidden; - line-height: 1.5em; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 160px; -} - -.gh-wn-entry .gh-wn-footer { - margin: 0 -32px -32px; - padding: 14px 32px 16px; - border-top: 1px solid var(--whitegrey); - justify-content: space-between; -} - -.gh-wn-footer { - position: relative; - margin-top: 14px; - margin-bottom: -13px; -} - -.gh-wn-footer:before { - position: absolute; - content: ""; - top: -14px; - left: -32px; - right: -32px; - height: 6px; - background: rgba(255,255,255,0); - box-shadow: - 0 -0.3px 1px rgba(0, 0, 0, 0.03), - 0 -4px 7px rgba(0, 0, 0, 0.06); -} - -.gh-about-container { - display: grid; - grid-template-columns: 2fr 1fr; - grid-gap: 80px; -} - -.gh-whats-new-canvas .gh-about-container { - display: flex; - grid-template-columns: unset; - grid-gap: unset; - margin: 0 auto; - max-width: 920px; - margin-top: 60px; -} - -.gh-about-container h2 { - font-size: 1.65rem; - line-height: 1.4em; - font-weight: 600; - border-bottom: 1px solid var(--lightgrey-l2); - padding-bottom: 12px; - margin-bottom: 12px; -} - -.gh-about-box { - position: sticky; - top: 96px; - right: 0; - display: flex; - flex-grow: 1; - flex-direction: column; - height: max-content; - border-radius: 3px; - min-width: 300px; -} - -.gh-about-box.grey { - border: none; - background: var(--main-color-content-greybg); -} - -@media (max-width: 1380px) { - .gh-wn-content { - max-width: 36vw; - } -} - -@media (max-width: 1120px) { - .gh-wn-content { - max-width: 680px; - } - - .gh-about-box { - position: relative; - top: unset; - right: unset; - } - - .gh-about-container { - grid-template-columns: unset; - grid-template-rows: auto; - grid-gap: 32px; - } - - .gh-whats-new { - grid-row: 3/4; - } - - - .gh-about-header-actions a { - display: none; - } - - .gh-wn-entry iframe { - max-width: 100%; - } -} - -/* Custom card styles -/* ---------------------------------------------------------- */ - -.gh-whats-new .kg-audio-card { - display: flex; - width: 100%; - min-height: 96px; - border-radius: 3px; - box-shadow: inset 0 0 0 1px rgba(124, 139, 154, 0.25); - margin-bottom: 1.5em; -} - -.gh-whats-new .kg-audio-card+.gh-whats-new .kg-audio-card { - margin-top: 1em; -} - -.gh-whats-new .kg-audio-thumbnail { - display: flex; - justify-content: center; - align-items: center; - width: 80px; - min-width: 80px; - margin: 8px; - background: transparent; - object-fit: cover; - aspect-ratio: 1/1; - border-radius: 2px; -} - -.gh-whats-new .kg-audio-thumbnail.placeholder { - background: var(--accent-color); -} - -.gh-whats-new .kg-audio-thumbnail.placeholder svg { - width: 24px; - height: 24px; - fill: white; -} - -.gh-whats-new .kg-audio-player-container { - position: relative; - display: flex; - flex-direction: column; - justify-content: space-between; - flex: 1; - --seek-before-width: 0%; - --volume-before-width: 100%; - --buffered-width: 0%; -} - -.gh-whats-new .kg-audio-title { - width: 100%; - margin: 8px 0 0 0; - padding: 8px 12px; - border: none; - font-family: inherit; - font-size: 1.15em; - font-weight: 700; - line-height: 1.15em; - background: transparent; -} - -.gh-whats-new .kg-audio-player { - display: flex; - flex-grow: 1; - align-items: center; - padding: 8px 12px; -} - -.gh-whats-new .kg-audio-current-time { - min-width: 38px; - padding: 0 4px; - font-family: inherit; - font-size: .85em; - font-weight: 500; - line-height: 1.4em; - white-space: nowrap; -} - -.gh-whats-new .kg-audio-time { - width: 56px; - color: #ababab; - font-family: inherit; - font-size: .85em; - font-weight: 500; - line-height: 1.4em; - white-space: nowrap; -} - -.gh-whats-new .kg-audio-duration { - padding: 0 4px; -} - -.gh-whats-new .kg-audio-play-icon, -.gh-whats-new .kg-audio-pause-icon { - position: relative; - bottom: 1px; - padding: 0px 4px 0 0; - font-size: 0; - background: transparent; -} - -.gh-whats-new .kg-audio-hide { - display: none !important; -} - -.gh-whats-new .kg-audio-play-icon svg, -.gh-whats-new .kg-audio-pause-icon svg { - width: 14px; - height: 14px; - fill: currentColor; -} - -.gh-whats-new .kg-audio-seek-slider { - flex-grow: 1; - margin: 0 4px; - width: 100%; -} - -@media (max-width: 640px) { - .gh-whats-new .kg-audio-seek-slider { - display: none; - } -} - -.gh-whats-new .kg-audio-playback-rate { - min-width: 37px; - padding: 0 4px; - font-family: inherit; - font-size: .85em; - font-weight: 600; - line-height: 1.4em; - text-align: left; - background: transparent; - white-space: nowrap; -} - -@media (max-width: 640px) { - .gh-whats-new .kg-audio-playback-rate { - padding-left: 8px; - } -} - -.gh-whats-new .kg-audio-mute-icon, -.gh-whats-new .kg-audio-unmute-icon { - position: relative; - bottom: -1px; - padding: 0 4px; - font-size: 0; - background: transparent; -} - -@media (max-width: 640px) { - .gh-whats-new .kg-audio-mute-icon, - .gh-whats-new .kg-audio-unmute-icon { - margin-left: auto; - } -} - -.gh-whats-new .kg-audio-mute-icon svg, -.gh-whats-new .kg-audio-unmute-icon svg { - width: 16px; - height: 16px; - fill: currentColor; -} - -.gh-whats-new .kg-audio-volume-slider { - flex-grow: 1; - width: 100%; - min-width: 50px; - max-width: 80px; -} - -@media (max-width: 400px) { - .gh-whats-new .kg-audio-volume-slider { - display: none; - } -} - -.gh-whats-new .kg-audio-seek-slider::before { - content: ""; - position: absolute; - left: 0; - width: var(--seek-before-width) !important; - height: 4px; - cursor: pointer; - background-color: currentColor; - border-radius: 2px; -} - -.gh-whats-new .kg-audio-volume-slider::before { - content: ""; - position: absolute; - left: 0; - width: var(--volume-before-width) !important; - height: 4px; - cursor: pointer; - background-color: currentColor; - border-radius: 2px; -} - -/* Resetting browser styles -/* --------------------------------------------------------------- */ - -.gh-whats-new .kg-audio-player-container input[type=range] { - position: relative; - -webkit-appearance: none; - background: transparent; -} - -.gh-whats-new .kg-audio-player-container input[type=range]:focus { - outline: none; -} - -.gh-whats-new .kg-audio-player-container input[type=range]::-webkit-slider-thumb { - -webkit-appearance: none; -} - -.gh-whats-new .kg-audio-player-container input[type=range]::-ms-track { - cursor: pointer; - border-color: transparent; - color: transparent; - background: transparent; -} - -.gh-whats-new .kg-audio-player-container button { - display: flex; - align-items: center; - border: 0; - cursor: pointer; -} - -.gh-whats-new .kg-audio-player-container input[type="range"] { - height: auto; - padding: 0; - border: 0; -} - -/* Chrome & Safari styles -/* --------------------------------------------------------------- */ - -.gh-whats-new .kg-audio-player-container input[type="range"]::-webkit-slider-runnable-track { - width: 100%; - height: 4px; - cursor: pointer; - background: rgba(124, 139, 154, 0.25); - border-radius: 2px; -} - -.gh-whats-new .kg-audio-player-container input[type="range"]::-webkit-slider-thumb { - position: relative; - box-sizing: content-box; - width: 13px; - height: 13px; - margin: -5px 0 0 0; - border: 0; - cursor: pointer; - background: #fff; - border-radius: 50%; - box-shadow: 0 0 0 1px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,0.24); -} - -.gh-whats-new .kg-audio-player-container input[type="range"]:active::-webkit-slider-thumb { - transform: scale(1.2); -} - -/* Firefox styles -/* --------------------------------------------------------------- */ - -.gh-whats-new .kg-audio-player-container input[type="range"]::-moz-range-track { - width: 100%; - height: 4px; - cursor: pointer; - background: rgba(124, 139, 154, 0.25); - border-radius: 2px; -} - -.gh-whats-new .kg-audio-player-container input[type="range"]::-moz-range-progress { - background: currentColor; - border-radius: 2px; -} - -.gh-whats-new .kg-audio-player-container input[type="range"]::-moz-range-thumb { - box-sizing: content-box; - width: 13px; - height: 13px; - border: 0; - cursor: pointer; - background: #fff; - border-radius: 50%; - box-shadow: 0 0 0 1px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,0.24); -} - -.gh-whats-new .kg-audio-player-container input[type="range"]:active::-moz-range-thumb { - transform: scale(1.2); -} - -/* Edge & IE styles -/* --------------------------------------------------------------- */ - -.gh-whats-new .kg-audio-player-container input[type="range"]::-ms-track { - width: 100%; - height: 3px; - border: solid transparent; - color: transparent; - cursor: pointer; - background: transparent; -} - -.gh-whats-new .kg-audio-player-container input[type="range"]::-ms-fill-lower { - background: #fff; -} - -.gh-whats-new .kg-audio-player-container input[type="range"]::-ms-fill-upper { - background: currentColor; -} - -.gh-whats-new .kg-audio-player-container input[type="range"]::-ms-thumb { - box-sizing: content-box; - width: 13px; - height: 13px; - border: 0; - cursor: pointer; - background: #fff; - border-radius: 50%; - box-shadow: 0 0 0 1px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,0.24); -} - -.gh-whats-new .kg-audio-player-container input[type="range"]:active::-ms-thumb { - transform: scale(1.2); -} - -.gh-whats-new .kg-product-card { - display: flex; - align-items: center; - flex-direction: column; - width: 100%; - margin-bottom: 1.5em; -} - -.gh-whats-new .kg-product-card-container { - display: grid; - grid-template-columns: auto min-content; - align-items: center; - grid-row-gap: 16px; - background: transparent; - max-width: 550px; - width: 100%; -} - -.gh-whats-new .kg-product-card-image { - grid-column: 1 / 3; - justify-self: center; -} - -.gh-whats-new .kg-product-card-title-container { - grid-column: 1 / 2; -} - -.gh-whats-new .kg-product-card h4.kg-product-card-title { - font-family: var(--font-family); - text-decoration: none; - font-weight: 700; - font-size: 1.4em; - margin-top: 0; - margin-bottom: 0; - line-height: 1.15em; - text-transform: none; - color: inherit; -} - -.gh-whats-new .kg-product-card-description { - grid-column: 1 / 3; -} - -.gh-whats-new .kg-product-card .kg-product-card-description p, -.gh-whats-new .kg-product-card .kg-product-card-description ol, -.gh-whats-new .kg-product-card .kg-product-card-description ul { - font-family: var(--font-family); - font-size: 0.9em; - line-height: 1.5em; - opacity: .7; -} - -.gh-whats-new .kg-product-card .kg-product-card-description p:not(:first-of-type) { - margin-top: 0.8em; - margin-bottom: 0; -} - -.gh-whats-new .kg-product-card .kg-product-card-description p:first-of-type { - margin-top: -4px; -} - -.gh-whats-new .kg-product-card .kg-product-card-description ul, -.gh-whats-new .kg-product-card .kg-product-card-description ol { - margin-top: 0.95em; -} - -.gh-whats-new .kg-product-card .kg-product-card-description li+li { - margin-top: 0.2em; -} - -.gh-whats-new .kg-product-card-rating { - display: flex; - align-items: center; - grid-column: 2 / 3; - align-self: start; - justify-self: end; - padding-left: 16px; -} - -@media (max-width: 400px) { - .gh-whats-new .kg-product-card-title-container { - grid-column: 1 / 3; - } - - .gh-whats-new .kg-product-card-rating { - grid-column: 1 / 3; - justify-self: start; - margin-top: -15px; - padding-left: 0; - } -} - -.gh-whats-new .kg-product-card-rating-star { - height: 28px; - width: 20px; -} - -.gh-whats-new .kg-product-card-rating-star svg { - width: 16px; - height: 16px; - fill: currentColor; - opacity: 0.15; -} - -.gh-whats-new .kg-product-card-rating-star svg path { - fill: unset; -} - -.gh-whats-new .kg-product-card-rating-active.kg-product-card-rating-star svg { - opacity: 1; -} - -.gh-whats-new .kg-product-card a.kg-product-card-button { - justify-content: center; - grid-column: 1 / 3; - display: flex; - position: static; - align-items: center; - font-family: var(--font-family); - font-size: 0.95em; - font-weight: 600; - line-height: 1em; - text-decoration: none; - width: 100%; - height: 2.4em; - border-radius: 5px; - padding: 0 1.2em; - transition: opacity 0.2s ease-in-out; - margin: 0; -} - -.gh-whats-new .kg-product-card a.kg-product-card-btn-accent { - background-color: var(--accent-color); - color: #fff; -} - -.gh-whats-new .kg-blockquote-alt { - font-size: 1.5em; - font-style: italic; - line-height: 1.7em; - text-align: center; - padding: 0 2.5em; -} - -@media (max-width: 800px) { - .gh-whats-new .kg-blockquote-alt { - font-size: 1.4em; - padding-left: 2em; - padding-right: 2em; - } -} - -@media (max-width: 600px) { - .gh-whats-new .kg-blockquote-alt { - font-size: 1.2em; - padding-left: 1.75em; - padding-right: 1.75em; - } -} - -.gh-whats-new .kg-button-card { - display: flex; - position: static; - align-items: center; - width: 100%; - justify-content: flex-start; - padding: 30px 0; -} - -.gh-whats-new .kg-button-card.kg-align-left { - justify-content: flex-start; -} - -.gh-whats-new .kg-button-card a.kg-btn { - display: flex; - position: static; - align-items: center; - padding: 0 1.2em; - height: 2.4em; - line-height: 1em; - font-family: var(--font-family); - font-size: 0.95em; - font-weight: 600; - text-decoration: none; - border-radius: 5px; - transition: opacity 0.2s ease-in-out; -} - -.gh-whats-new .kg-button-card a.kg-btn:hover { - opacity: 0.85; -} - -.gh-whats-new .kg-button-card a.kg-btn-accent { - background-color: var(--accent-color); - color: #fff; -} - -.gh-whats-new .kg-callout-card { - display: flex; - padding: 1.2em 1.6em; - border-radius: 3px; -} - -.gh-whats-new .kg-callout-card-grey { - background: rgba(124, 139, 154, 0.13); -} - -.gh-whats-new .kg-callout-card-white { - background: transparent; - box-shadow: inset 0 0 0 1px rgba(124, 139, 154, 0.25); -} - -.gh-whats-new .kg-callout-card-blue { - background: rgba(33, 172, 232, 0.12); -} - -.gh-whats-new .kg-callout-card-green { - background: rgba(52, 183, 67, 0.12); -} - -.gh-whats-new .kg-callout-card-yellow { - background: rgba(240, 165, 15, 0.13); -} - -.gh-whats-new .kg-callout-card-red { - background: rgba(209, 46, 46, 0.11); -} - -.gh-whats-new .kg-callout-card-pink { - background: rgba(225, 71, 174, 0.11); -} - -.gh-whats-new .kg-callout-card-purple { - background: rgba(135, 85, 236, 0.12); -} - -.gh-whats-new .kg-callout-card-accent { - background: var(--ghost-accent-color); - color: #fff; -} - -.gh-whats-new .kg-callout-card-accent a { - color: #fff; -} - -.gh-whats-new .kg-callout-card div.kg-callout-emoji { - padding-right: .8em; - line-height: 1.25em; - font-size: 1.15em; -} - -.gh-whats-new .kg-callout-card div.kg-callout-text { - font-size: .95em; - line-height: 1.5em; -} - -.gh-whats-new .kg-callout-card + .kg-callout-card { - margin-top: 1em; -} - -.gh-whats-new .kg-file-card { - display: flex; -} - -.gh-whats-new .kg-file-card a.kg-file-card-container { - display: flex; - align-items: center; - justify-content: space-between; - color: inherit; - padding: 6px; - min-height: 92px; - border: 1px solid rgb(124 139 154 / 25%); - border-radius: 3px; - transition: all ease-in-out 0.35s; - text-decoration: none; - width: 100%; -} - -.gh-whats-new .kg-file-card a.kg-file-card-container:hover { - border: 1px solid rgb(124 139 154 / 35%); -} - -.gh-whats-new .kg-file-card-contents { - display: flex; - flex-direction: column; - justify-content: space-between; - margin: 4px 8px; -} - -.gh-whats-new .kg-file-card-title { - font-size: 1.15em; - font-weight: 700; - line-height: 1.3em; -} - -.gh-whats-new .kg-file-card-caption { - font-size: 0.95em; - line-height: 1.5em; - opacity: 0.6; -} - -.gh-whats-new .kg-file-card-metadata { - display: inline; - font-size: 0.825em; - line-height: 1.5em; - margin-top: 2px; -} - -.gh-whats-new .kg-file-card-filename { - display: inline; - font-weight: 500; -} - -.gh-whats-new .kg-file-card-filesize { - display: inline-block; - font-size: 0.925em; - opacity: 0.6; -} - -.gh-whats-new .kg-file-card-filesize:before { - display: inline-block; - content: "\2022"; - margin-right: 4px; -} - -.gh-whats-new .kg-file-card-icon { - position: relative; - display: flex; - align-items: center; - justify-content: center; - width: 80px; - min-width: 80px; - height: 100%; -} - -.gh-whats-new .kg-file-card-icon:before { - position: absolute; - display: block; - content: ""; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: currentColor; - opacity: 0.06; - transition: opacity ease-in-out 0.35s; - border-radius: 2px; -} - -.gh-whats-new .kg-file-card a.kg-file-card-container:hover .kg-file-card-icon:before { - opacity: 0.08; -} - -.gh-whats-new .kg-file-card-icon svg { - width: 24px; - height: 24px; - color: var(--ghost-accent-color); -} - -/* Size variations */ -.gh-whats-new .kg-file-card-medium a.kg-file-card-container { - min-height: 72px; -} - -.gh-whats-new .kg-file-card-medium .kg-file-card-caption { - opacity: 1.0; - font-weight: 500; -} - -.gh-whats-new .kg-file-card-small a.kg-file-card-container { - min-height: 52px; -} - -.gh-whats-new .kg-file-card-small .kg-file-card-metadata { - font-size: 1.0em; - margin-top: 0; -} - -.gh-whats-new .kg-file-card-small .kg-file-card-icon svg { - width: 20px; - height: 20px; -} - -.gh-whats-new .kg-file-card + .kg-file-card { - margin-top: 1em; -} - -.gh-whats-new .kg-nft-card { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - margin-left: auto; - margin-right: auto; -} - -.gh-whats-new .kg-nft-card a.kg-nft-card-container { - position: static; - display: flex; - flex: auto; - flex-direction: column; - text-decoration: none; - font-family: var(--font-family); - font-size: 14px; - font-weight: 400; - box-shadow: 0 2px 6px -2px rgb(0 0 0 / 10%), 0 0 1px rgb(0 0 0 / 40%); - width: 100%; - max-width: 512px; - color: #222; - background: #fff; - border-radius: 5px; - transition: none; -} - -.gh-whats-new .kg-nft-card * { - position: static; -} - -.gh-whats-new .kg-nft-metadata { - padding: 20px; - width: 100%; -} - -.gh-whats-new .kg-nft-image { - border-radius: 5px 5px 0 0; - width: 100%; -} - -.gh-whats-new .kg-nft-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 20px; -} - -.gh-whats-new .kg-nft-header h4.kg-nft-title { - font-family: inherit; - font-size: 19px; - font-weight: 700; - line-height: 1.3em; - min-width: unset; - max-width: unset; - margin: 0; - color: #222; -} - -.gh-whats-new .kg-nft-opensea-logo { - margin-top: 2px; - width: 100px; - object-fit: scale-down; -} - -.gh-whats-new .kg-nft-creator { - font-family: inherit; - line-height: 1.4em; - margin: 4px 0 0; - color: #ababab; -} - -.gh-whats-new .kg-nft-creator span { - font-weight: 500; - color: #222; -} - -.gh-whats-new .kg-nft-card p.kg-nft-description { - font-family: inherit; - font-size: 14px; - line-height: 1.4em; - margin: 20px 0 0; - color: #222; -} - -.gh-whats-new .kg-toggle-card { - background: transparent; - box-shadow: inset 0 0 0 1px rgba(124, 139, 154, 0.25); - border-radius: 4px; - padding: 1.2em; -} - -.gh-whats-new .kg-toggle-card[data-kg-toggle-state="close"] .kg-toggle-content{ - height: 0; - overflow: hidden; - transition: opacity .5s ease, top .35s ease; - opacity: 0; - top: -0.5em; - position: relative; -} - -.gh-whats-new .kg-toggle-content { - height: auto; - opacity: 1; - transition: opacity 1s ease, top .35s ease; - top: 0; - position: relative; -} - -.gh-whats-new .kg-toggle-card[data-kg-toggle-state="close"] svg { - transform: unset; -} - -.gh-whats-new .kg-toggle-heading { - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: flex-start; -} - -.gh-whats-new .kg-toggle-card h4.kg-toggle-heading-text { - font-size: 1.15em; - font-weight: 700; - line-height: 1.3em; - margin-top: 0; - margin-bottom: 0; - text-transform: none; - color: inherit; -} - -.gh-whats-new .kg-toggle-content p:first-of-type { - margin-top: 0.5em; -} - -.gh-whats-new .kg-toggle-card .kg-toggle-content p, -.gh-whats-new .kg-toggle-card .kg-toggle-content ol, -.gh-whats-new .kg-toggle-card .kg-toggle-content ul { - font-size: 0.95em; - line-height: 1.5em; - margin-top: 0.95em; -} - -.gh-whats-new .kg-toggle-card li + li { - margin-top: 0.5em; -} - -.gh-whats-new .kg-toggle-card-icon { - height: 24px; - width: 24px; - display: flex; - justify-content: center; - align-items: center; - margin-left: 1em; - background: none; - border: 0; -} - -.gh-whats-new .kg-toggle-heading svg { - width: 14px; - color: rgba(124, 139, 154, 0.5); - transition: all 0.3s; - transform: rotate(-180deg); -} - -.gh-whats-new .kg-toggle-heading path { - fill: none; - stroke: currentcolor; - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 1.5; - fill-rule: evenodd; -} - -.gh-whats-new .kg-toggle-card + .kg-toggle-card { - margin-top: 1em; -} - -.gh-whats-new .kg-video-card { - position: relative; - --seek-before-width: 0%; - --volume-before-width: 100%; - --buffered-width: 0%; -} - -.gh-whats-new .kg-video-card video { - display: block; - max-width: 100%; - height: auto; -} - -.gh-whats-new .kg-video-container { - position: relative; - display: flex; - flex-direction: column; - align-items: center; -} - -.gh-whats-new .kg-video-overlay { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - display: flex; - justify-content: center; - align-items: center; - background-image: linear-gradient(180deg,rgba(0,0,0,0.3) 0,transparent 70%,transparent 100%); - z-index: 99; - transition: opacity .2s ease-in-out; -} - -.gh-whats-new .kg-video-large-play-icon { - display: flex; - justify-content: center; - align-items: center; - width: 72px; - height: 72px; - padding: 0; - background: rgba(0, 0, 0, 0.5); - border-radius: 50%; - transition: opacity .2s ease-in-out; -} - -.gh-whats-new .kg-video-large-play-icon svg { - width: 20px; - height: auto; - margin-left: 2px; - fill: #fff; -} - -.gh-whats-new .kg-video-player-container { - position: absolute; - bottom: 0; - width: 100%; - height: 80px; - background: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,.5)); - z-index: 99; - transition: opacity .2s ease-in-out; - -} - -.gh-whats-new .kg-video-player { - position: absolute; - bottom: 0; - display: flex; - align-items: center; - width: 100%; - z-index: 99; - padding: 12px 16px; -} - -.gh-whats-new .kg-video-current-time { - min-width: 38px; - padding: 0 4px; - color: #fff; - font-family: inherit; - font-size: .85em; - font-weight: 500; - line-height: 1.4em; - white-space: nowrap; -} - -.gh-whats-new .kg-video-time { - color: rgba(255, 255, 255, 0.6); - font-family: inherit; - font-size: .85em; - font-weight: 500; - line-height: 1.4em; - white-space: nowrap; -} - -.gh-whats-new .kg-video-duration { - padding: 0 4px; -} - -.gh-whats-new .kg-video-play-icon, -.gh-whats-new .kg-video-pause-icon { - position: relative; - padding: 0px 4px 0 0; - font-size: 0; - background: transparent; -} - -.gh-whats-new .kg-video-hide { - display: none !important; -} - -.gh-whats-new .kg-video-hide-animated { - opacity: 0 !important; - transition: opacity .2s ease-in-out; - cursor: initial; -} - -.gh-whats-new .kg-video-play-icon svg, -.gh-whats-new .kg-video-pause-icon svg { - width: 14px; - height: 14px; - fill: #fff; -} - -.gh-whats-new .kg-video-seek-slider { - flex-grow: 1; - margin: 0 4px; -} - -@media (max-width: 520px) { - .gh-whats-new .kg-video-seek-slider { - display: none; - } -} - -.gh-whats-new .kg-video-playback-rate { - min-width: 37px; - padding: 0 4px; - color: #fff; - font-family: inherit; - font-size: .85em; - font-weight: 600; - line-height: 1.4em; - text-align: left; - background: transparent; - white-space: nowrap; -} - -@media (max-width: 520px) { - .gh-whats-new .kg-video-playback-rate { - padding-left: 8px; - } -} - -.gh-whats-new .kg-video-mute-icon, -.gh-whats-new .kg-video-unmute-icon { - position: relative; - bottom: -1px; - padding: 0 4px; - font-size: 0; - background: transparent; -} - -@media (max-width: 520px) { - .gh-whats-new .kg-video-mute-icon, - .gh-whats-new .kg-video-unmute-icon { - margin-left: auto; - } -} - -.gh-whats-new .kg-video-mute-icon svg, -.gh-whats-new .kg-video-unmute-icon svg { - width: 16px; - height: 16px; - fill: #fff; -} - -.gh-whats-new .kg-video-volume-slider { - width: 80px; -} - -@media (max-width: 300px) { - .gh-whats-new .kg-video-volume-slider { - display: none; - } -} - -.gh-whats-new .kg-video-seek-slider::before { - content: ""; - position: absolute; - left: 0; - width: var(--seek-before-width) !important; - height: 4px; - cursor: pointer; - background-color: #EBEEF0; - border-radius: 2px; -} - -.gh-whats-new .kg-video-volume-slider::before { - content: ""; - position: absolute; - left: 0; - width: var(--volume-before-width) !important; - height: 4px; - cursor: pointer; - background-color: #EBEEF0; - border-radius: 2px; -} - -/* Resetting browser styles -/* --------------------------------------------------------------- */ - -.gh-whats-new .kg-video-card input[type=range] { - position: relative; - -webkit-appearance: none; - background: transparent; -} - -.gh-whats-new .kg-video-card input[type=range]:focus { - outline: none; -} - -.gh-whats-new .kg-video-card input[type=range]::-webkit-slider-thumb { - -webkit-appearance: none; -} - -.gh-whats-new .kg-video-card input[type=range]::-ms-track { - cursor: pointer; - border-color: transparent; - color: transparent; - background: transparent; -} - -.gh-whats-new .kg-video-card button { - display: flex; - align-items: center; - border: 0; - cursor: pointer; -} - -.gh-whats-new .kg-video-card input[type="range"] { - height: auto; - padding: 0; - border: 0; -} - -/* Chrome & Safari styles -/* --------------------------------------------------------------- */ - -.gh-whats-new .kg-video-card input[type="range"]::-webkit-slider-runnable-track { - width: 100%; - height: 4px; - cursor: pointer; - background: rgba(255, 255, 255, 0.2); - border-radius: 2px; -} - -.gh-whats-new .kg-video-card input[type="range"]::-webkit-slider-thumb { - position: relative; - box-sizing: content-box; - width: 13px; - height: 13px; - margin: -5px 0 0 0; - border: 0; - cursor: pointer; - background: #fff; - border-radius: 50%; - box-shadow: 0 0 0 1px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,0.24); -} - -.gh-whats-new .kg-video-card input[type="range"]:active::-webkit-slider-thumb { - transform: scale(1.2); -} - -/* Firefox styles -/* --------------------------------------------------------------- */ - -.gh-whats-new .kg-video-card input[type="range"]::-moz-range-track { - width: 100%; - height: 4px; - cursor: pointer; - background: rgba(255, 255, 255, 0.2); - border-radius: 2px; -} - -.gh-whats-new .kg-video-card input[type="range"]::-moz-range-progress { - background: #EBEEF0; - border-radius: 2px; -} - -.gh-whats-new .kg-video-card input[type="range"]::-moz-range-thumb { - box-sizing: content-box; - width: 13px; - height: 13px; - border: 0; - cursor: pointer; - background: #fff; - border-radius: 50%; - box-shadow: 0 0 0 1px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,0.24); -} - -.gh-whats-new .kg-video-card input[type="range"]:active::-moz-range-thumb { - transform: scale(1.2); -} - -/* Edge & IE styles -/* --------------------------------------------------------------- */ - -.gh-whats-new .kg-video-card input[type="range"]::-ms-track { - width: 100%; - height: 3px; - border: solid transparent; - color: transparent; - cursor: pointer; - background: transparent; -} - -.gh-whats-new .kg-video-card input[type="range"]::-ms-fill-lower { - background: #fff; -} - -.gh-whats-new .kg-video-card input[type="range"]::-ms-fill-upper { - background: #EBEEF0; -} - -.gh-whats-new .kg-video-card input[type="range"]::-ms-thumb { - box-sizing: content-box; - width: 13px; - height: 13px; - border: 0; - cursor: pointer; - background: #fff; - border-radius: 50%; - box-shadow: 0 0 0 1px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,0.24); -} - -.gh-whats-new .kg-video-card input[type="range"]:active::-ms-thumb { - transform: scale(1.2); -} - -/* File card styles */ -.gh-whats-new .kg-file-card { - display: flex; -} - -.gh-whats-new .kg-file-card a.kg-file-card-container { - display: flex; - align-items: stretch; - justify-content: space-between; - color: inherit; - padding: 6px; - min-height: 92px; - border: 1px solid rgb(124 139 154 / 25%); - border-radius: 3px; - transition: all ease-in-out 0.35s; - text-decoration: none; - width: 100%; -} - -.gh-whats-new .kg-file-card a.kg-file-card-container:hover { - border: 1px solid rgb(124 139 154 / 35%); -} - -.gh-whats-new .kg-file-card-contents { - display: flex; - flex-direction: column; - justify-content: space-between; - margin: 4px 8px; - width: 100% -} - -.gh-whats-new .kg-file-card-title { - font-size: 1.15em; - font-weight: 700; - line-height: 1.3em; -} - -.gh-whats-new .kg-file-card-caption { - font-size: 0.95em; - line-height: 1.3em; - opacity: 0.6; -} - -.gh-whats-new .kg-file-card-title + .kg-file-card-caption { - margin-top: -6px; -} - -.gh-whats-new .kg-file-card-metadata { - display: inline; - font-size: 0.825em; - line-height: 1.3em; - margin-top: 2px; -} - -.gh-whats-new .kg-file-card-filename { - display: inline; - font-weight: 500; -} - -.gh-whats-new .kg-file-card-filesize { - display: inline-block; - font-size: 0.925em; - opacity: 0.6; -} - -.gh-whats-new .kg-file-card-filesize:before { - display: inline-block; - content: "\2022"; - margin-right: 4px; -} - -.gh-whats-new .kg-file-card-icon { - position: relative; - display: flex; - align-items: center; - justify-content: center; - width: 80px; - min-width: 80px; - height: 100%; -} - -.gh-whats-new .kg-file-card-icon:before { - position: absolute; - display: block; - content: ""; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: currentColor; - opacity: 0.06; - transition: opacity ease-in-out 0.35s; - border-radius: 2px; -} - -.gh-whats-new .kg-file-card a.kg-file-card-container:hover .kg-file-card-icon:before { - opacity: 0.08; -} - -.gh-whats-new .kg-file-card-icon svg { - width: 24px; - height: 24px; - color: var(--ghost-accent-color); -} - -.gh-whats-new .kg-file-card-medium a.kg-file-card-container { - min-height: 72px; -} - -.gh-whats-new .kg-file-card-medium .kg-file-card-caption { - opacity: 1.0; - font-weight: 500; -} - -.gh-whats-new .kg-file-card-small a.kg-file-card-container { - align-items: center; - min-height: 52px; -} - -.gh-whats-new .kg-file-card-small .kg-file-card-metadata { - font-size: 1.0em; - margin-top: 0; -} - -.gh-whats-new .kg-file-card-small .kg-file-card-icon svg { - width: 20px; - height: 20px; -} - -.gh-whats-new .kg-file-card + .kg-file-card { - margin-top: 1em; -} - -/* Header card */ - -.gh-whats-new .kg-header-card { - padding: 12vmin 4em; - min-height: 20vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - margin-bottom: 1.5em; -} - -.gh-whats-new .kg-header-card.kg-size-small { - padding-top: 8vmin; - padding-bottom: 8vmin; - min-height: 12vh; -} - -.gh-whats-new .kg-header-card.kg-size-large { - padding-top: 12vmin; - padding-bottom: 12vmin; - min-height: 40vh; -} - -.gh-whats-new .kg-header-card.kg-align-left { - text-align: left; - align-items: flex-start; -} - -.gh-whats-new .kg-header-card.kg-style-dark { - background: #151515; - color: #ffffff; -} - -.gh-whats-new .kg-header-card.kg-style-light { - background-color: #fafafa; -} - -.gh-whats-new .kg-header-card.kg-style-accent { - background-color: var(--accent-color); -} - -.gh-whats-new .kg-header-card.kg-style-image { - position: relative; - background-color: #e7e7e7; - background-size: cover; - background-position: center; -} - -.gh-whats-new .kg-header-card.kg-style-image::before { - position: absolute; - display: block; - content: ""; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: linear-gradient(0deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.2)); -} - -.gh-whats-new .kg-header-card h2.kg-header-card-header { - font-size: 5em; - font-weight: 700; - line-height: 1.1em; - letter-spacing: -0.01em; - margin: 0; -} - -.gh-whats-new .kg-header-card h2.kg-header-card-header strong { - font-weight: 800; -} - -.gh-whats-new .kg-header-card.kg-size-small h2.kg-header-card-header { - font-size: 4em; -} - -.gh-whats-new .kg-header-card.kg-size-large h2.kg-header-card-header { - font-size: 6em; -} - -.gh-whats-new .kg-header-card h3.kg-header-card-subheader { - font-size: 1.5em; - font-weight: 500; - line-height: 1.4em; - margin: 0; - max-width: 40em; -} - -.gh-whats-new .kg-header-card h2 + h3.kg-header-card-subheader { - margin: 0.35em 0 0; -} - -.gh-whats-new .kg-header-card h3.kg-header-card-subheader strong { - font-weight: 600; -} - -.gh-whats-new .kg-header-card.kg-size-small h3.kg-header-card-subheader { - font-size: 1.25em; -} - -.gh-whats-new .kg-header-card.kg-size-large h3.kg-header-card-subheader { - font-size: 1.75em; -} - -.gh-whats-new .kg-header-card:not(.kg-style-light) h2.kg-header-card-header, -.gh-whats-new .kg-header-card:not(.kg-style-light) h3.kg-header-card-subheader { - color: #ffffff; -} - -.gh-whats-new .kg-header-card.kg-style-accent h3.kg-header-card-subheader, -.gh-whats-new .kg-header-card.kg-style-image h3.kg-header-card-subheader { - opacity: 1.0; -} - -.gh-whats-new .kg-header-card.kg-style-image h2.kg-header-card-header, -.gh-whats-new .kg-header-card.kg-style-image h3.kg-header-card-subheader, -.gh-whats-new .kg-header-card.kg-style-image a.kg-header-card-button { - z-index: 99; -} - -.gh-whats-new .kg-header-card h2.kg-header-card-header a, -.gh-whats-new .kg-header-card h3.kg-header-card-subheader a { - color: var(--ghost-accent-color); -} - -.gh-whats-new .kg-header-card.kg-style-accent h2.kg-header-card-header a, -.gh-whats-new .kg-header-card.kg-style-accent h3.kg-header-card-subheader a, -.gh-whats-new .kg-header-card.kg-style-image h2.kg-header-card-header a, -.gh-whats-new .kg-header-card.kg-style-image h3.kg-header-card-subheader a { - color: #fff; -} - -.gh-whats-new .kg-header-card a.kg-header-card-button { - display: flex; - position: static; - align-items: center; - fill: #fff; - background: #fff; - border-radius: 3px; - outline: none; - font-family: var(--font-family); - font-size: 1.05em; - font-weight: 600; - line-height: 1em; - text-align: center; - text-decoration: none; - letter-spacing: .2px; - white-space: nowrap; - text-overflow: ellipsis; - color: #151515; - height: 2.7em; - padding: 0 1.2em; - transition: opacity .2s ease; -} - -.gh-whats-new .kg-header-card h2 + a.kg-header-card-button, -.gh-whats-new .kg-header-card h3 + a.kg-header-card-button { - margin: 1.75em 0 0; -} - -.gh-whats-new .kg-header-card a.kg-header-card-button:hover { - opacity: 0.85; -} - -.gh-whats-new .kg-header-card.kg-size-large a.kg-header-card-button { - font-size: 1.1em; - height: 2.9em; -} - -.gh-whats-new .kg-header-card.kg-size-large h2 + a.kg-header-card-button, -.gh-whats-new .kg-header-card.kg-size-large h3 + a.kg-header-card-button { - margin-top: 2em; -} - -.gh-whats-new .kg-header-card.kg-size-small a.kg-header-card-button { - height: 2.4em; - font-size: 1em; -} - -.gh-whats-new .kg-header-card.kg-size-small h2 + a.kg-header-card-button, -.gh-whats-new .kg-header-card.kg-size-small h3 + a.kg-header-card-button { - margin-top: 1.5em; -} - -.gh-whats-new .kg-header-card.kg-style-image a.kg-header-card-button, -.gh-whats-new .kg-header-card.kg-style-dark a.kg-header-card-button { - background: #fff; - color: #151515; -} - -.gh-whats-new .kg-header-card.kg-style-light a.kg-header-card-button { - background: var(--ghost-accent-color); - color: #fff; -} - -.gh-whats-new .kg-header-card.kg-style-accent a.kg-header-card-button { - background: #fff; - color: #151515; -} - diff --git a/apps/admin-x-activitypub/src/App.tsx b/apps/admin-x-activitypub/src/App.tsx index 983751ea458..efb1b2fdd22 100644 --- a/apps/admin-x-activitypub/src/App.tsx +++ b/apps/admin-x-activitypub/src/App.tsx @@ -1,4 +1,4 @@ -import MainContent from './MainContent'; +import ListIndex from './components/ListIndex'; import {DesignSystemApp, DesignSystemAppProps} from '@tryghost/admin-x-design-system'; import {FrameworkProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework'; import {RoutingProvider} from '@tryghost/admin-x-framework/routing'; @@ -8,25 +8,16 @@ interface AppProps { designSystem: DesignSystemAppProps; } -const modals = { - paths: { - 'follow-site': 'FollowSite', - 'view-following': 'ViewFollowing', - 'view-followers': 'ViewFollowers' - }, - load: async () => import('./components/modals') -}; - const App: React.FC = ({framework, designSystem}) => { return ( - + - + ); }; -export default App; \ No newline at end of file +export default App; diff --git a/apps/admin-x-activitypub/src/MainContent.tsx b/apps/admin-x-activitypub/src/MainContent.tsx deleted file mode 100644 index 31ddedf3f88..00000000000 --- a/apps/admin-x-activitypub/src/MainContent.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import ActivityPubComponent from './components/ListIndex'; - -const MainContent = () => { - return ; -}; - -export default MainContent; diff --git a/apps/admin-x-activitypub/src/assets/images/ap-welcome.png b/apps/admin-x-activitypub/src/assets/images/ap-welcome.png deleted file mode 100644 index 189768bcc5de3732fffe01d8c895eca90cd93110..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81406 zcmc$Fg;!f&&~1@Yq|g$ig`&azheL2rf&~2+J#+T#6RD-4LQF_a_~_9iVhC7K=g}jqDa=)YkB7PA z0A>xxd=R_?8@fGuM9K2+!h+~9|H0hEa???ffAo2bW(V^H*G^7d?$M*_c%oa2$B(e2 zk|2t5FmJ5=JA!N$xc^0b;NI%e(!NBG0MRfzmYiH+B-ghCO%uzQ#0@*e+)@VVZZaW+ zn#srOVJ5^3SO}S}c*jy;CB*8Y6v5u~;o|@$`O9<*+m~Tf#eTNFofqF9ZlwJ?gtztP z4t6q{SNvPI-w@LGTGBxJxn$Hv$Dc(rfmB+ zmb%Xite%D4Hb;+1WLUAad@FEMQB+i;2O^b6sK`mD6*u%1KaWO?fyJ5amegI>w7aCw zjfuAh@039e#yw|o5?+T!Q^Cmh%ySVDXcgwE2=SmclwMr`JHQ4jU-(Ufh$f<8RkqD4 zErm44bh~M-`d19i!9%qZql1R+Ypqim75UhRbdVx2Ha%q^sYel)Dj~_7mXDv3MwdNE z1SwKksRbeqXywS?suG=FOV=WVO`T_Xj^UwOGN{E85sHeA+KCYnh7nrVgapj#Z)8MI za0TKbx_6F;`83~L@jV>#k!f0f&xOQR)V?|Slk7<%A#>yQ`H#^z%A~M8i-j?}IYcKt z&Ga9uotJ={2iK)uXY>CoO4pb=8B2lG{7(@Paw#`_8H2N?os}V31k+>S(}yG@}0c zuB)qSDV5vs_!F`XdF8iuvzq)6?W6Pj(29-Q$c`>j2#EP_b~!R(Yr$C?vz0y8Do>By znaRn??eB>CVUHV}9d5aHvrhG@8)rJ74P=r6g_{`UQ<+BpOdB!h>L0UWak%$CulCX? z82w;K+S(5QfD5kIp;PKJ zShjmo&sl$0KaxE?Q73;iYRfWn;o<2SKr(JF8H7_(#?8@Zog@#&7>{}mZzXjKt%Ed$ z*}+eiJ#t&1pNNl~U&iT_&q_z_>c#;8kG*M5WcD8J>FDE(wpsD-n~MIIlJ< zhXCVu&(!_t00;n?@IP;j=%5}a+8bqW@v1fJOK)epae}H8A?}h1YR#e+Pn|Rbh-n+Y zNj-^-h!A7v1dvWDgP(!r4PD+ksClG$9I{4AU;i$gS6riInfxm&FzYv6e$trOJ+VKq zdc__Xn3d>|ti}Y)WGeGyTEyPLQUMasM0FFgr80>!X~}+Gbo|`B^`2$&0<6e*x@rOv z6T4jeXC}*G+Bp4w!(#xtb-S5R&_Ubi+y6qWh(bq_#w*>s(A^boRnlFP;v5ODT%D>p zdNn4JFsWx!rxP5$N1SF9xkWG4URk+j>COGaX8C_U;`vX`E@u}1F|#9^^6~XvRhs|J zIm>VwU!bT3J;3EZZt?SZD}pDkqB?^@)f;zav1Twy$x#Oa%(@W%%e!yLJ(NO~_S~Ur zZZm|SuFheab(`JoSFC6*{Xs!e_#z~4WohY#rae54JD5Cb*^ODZF^|4UDA_hTBEmin zj3O0`JN=-mExn|XE-@#}5V_cJczEaq>1DK-EKthxIZn#kPlYVLK43-S?TKg>J4>BFIcu1KuXF_f&^pM zUp$hHKXL%%oo(gb-31nTyWB+Lm!~E;wfF@_f7KDQ`XG)zyzSbEf3?Cj+y54>{YQ^# z$lfwj;SCJmu1@>f!Ip!@b-}fBz?ZJYe-C_u|g@ZF8SE-HvRZDdLN<0S$w# zL{vojBklTE{8sk1ZbYzXNP;_Km#UY&J^R(wc;NjU20olqcUrZmnvdw+uT{{`-lXXP zcf^%j0uBUr3~mS^Ls9~!rF!K@bAFdtDPUTTT=yx6hh%}FGR_Yt)GsiM8Q`F(NQW`W zmfVK}_D!BQ8_J)oyzrr)_n_YBU`q$>UaW?r4EKzoO z;S){{gxl-YNlNTrd8*9g5fKFc8_1;dtIOQWVL~nMe#*~9K700ztz5q<*A-BC;hcK^ z5D*zt%H!U`-{MnvYVP+Lu&0-S$aWp#7+=p7023^!Qxi*iF#&T#lmbXann zWXvlipv_gs&0&(y$(A}gNxS`sj_PuCm^Oos91;$T^Y3u4@M*3CocUi2em(0fpZlUz z3_vjX1KkE@4y$7zd66t#|9SW&R4_t|NlC^3bq)NuLHDUBX%?(tQm(;SEE46BxhnHK zwIZWAw0_{b+?jAMTKBav;;OXAd~JGLQg?Ypdm~;DoeZ{0MgMo<&_2G@>(vJ&`s3=b z$1Ea63wS~E?l-WQ{0ZNyfk)BGQQ{aQqDcSc^>g=;g^k&$$g1P+x4c3)=)p?n!DMSu z*%Z(psT6FLb2Y@n@swau-4EtQlU-?L&i*Qqs+Vxj zvxMCIG5Wb^GMDxrp#O7G12zDi7WQR@1y$QKh%b5hBN&WwEb3#BcK`8h4($IM385FP z*)eB%+Pc5|4ypDyA@0fB_a3P{=stswibiPIK0QRfq|oHh)HtqeB_e|6U;Bp79RpnB z&nk?@f)F?)lRPy2iYpX#ZdL@Hp2&E44l2kwU5uj|_O{-%x7~8+7@c}&5WeQ3Z8>3@ zLi}zwH>sLiX|ESiwU76m>p`Tp9B5O=M!by2Ae2*(?0R*aq?(o{c98YW`FB=@ciSs} z*T!_u6OiAnH#!2}+V4tUVP|m0yuHqjc5o7ID&gE|waFWi?h{d$ME5sY6~L%TCnNrW z9X8S7GweITdfvZ5SoQ=tkDP7{4mbi52duO|u@e7CPpn0E}gbwhue z-xis&jx>D1L^$#s6L^F=5VImBh@H&Z$#1Lw^R-+Frd1I*iYP&)%ML)V?f?5 z&^(fjK+3aF@&7$7-pPDD3H1#-rxKALV?HP`v5ZdlVBfTak4M2&`S3oShC`XY$!Ka< zwqdpUP%;A=vwRZ|{ZTpm9;MTf^@K!)M@6(n(H?o;07j5p|e@s}U9*ws3brU%#=C=NvGSzPJ|mTdUMnK>;~ zdgY#~AY|G#iV2x60`)li+hvV{%x~(=jm^qucZ+n;iwg^e`e!~KuU~&^ao?G`zPng6 zZ$F(Kn{rFV*rt@wj6?8b_H*K=t(pfBdRKAzEpdGnw9=#f0geyPQ-yn>`O$<;tfZtW;suJ-{J;K-H%(>KQ6+qh z#_AF4*ZcI{&uh5doa3Ap;IIt|WLM~HGASmkH z^#NG2%%F4j`T03ue5q*Iw2VZ9L5~)9mbdB0N9`1Qm9xvq*jJ)@^_BIaB0x(@hQ)tC zzB(DfeD;}4rLr905GAy@vXTP<_ezwx&j9SwcE3L)>sh%kHNH}?(CCm&j8hVTmmW5m z8DXV}?D<_BhAQrXUZeL;O8NC`Di~wWFD~Zro;-0ZBQ52OELf#g;#8uoK)k+=e=Ftr z*QKAf@ihM7GCr;IdbY|8HD03&si*0&2F6kT3vtp7(87r03ukQ_(+YPZ{165Qtlc|Z ztpe^!5L!q<2N`$v5U)moh+ko?e8i~qR7-2EjNC|naEeFJPUqO&iL+ilTez4-HFXAN zR4M;?@h}2jG)ry?k95CJN$$CGurm-mVtyaoWZ5sr{PS#*(c`znnTh@}$@}M?jqsMqW37a1u6ZL{& zMOrGk&~%O|w$CWNLo> z{{y~?FqAD5v7~1tlgF_4e$Dt$3fCrBPASFRy^4kddZLkVHGY%oo^vlkA-&z&aQ&l4 zXXX9?=Z0Gv&e@;Mq#NYE!OwDV=Zg9uG^@dRb8(bLE8!^ywCOfmoSS1yV6!F+S~R)P zjD5W@wj4pBqhtgpm_>gy)Q8lo&i1J$=#0xe-1=O{3PY;wLYFr)uMc`eb6nHR-Wi1T9@lp2FU~OtC%PbDjSyC2gk{CRG715Mlv9tv0Ho-2nI7Gbsw?oz*~ z#tlxpUv#{ig|Ow+nZ%Ln+T?S2pxu2VM;p5_lq{Cgw-xNND zr6(xA;4|Y^O^tzb${WXqH*DqdyOyea#cpp{|peUui%HJF?i zi?%z+C2KI*3a$^o;!gsr1M7$QR+Wvkr?M|@w`BtRuFv-LKatQ-QSE%KR*HLG#8EMZ zJ*1k)yvb5hPUvI&UAedrjUNi(BtlVqkaaSQOksXmaM5ZpkRKxU7T+?_zI2zk2$p24 zzT!xtCY_XsQDBjS(C(sK%fq1@ohq z)YL%6V4sh4E|fV_x55H&hK$=kQu;$*GV3Ji^Sn`z58mXH;{ih8OJByzM_e61A{kT+jH=^;m37DT-Y5 zR|6_rXxus-C%b<8@D?pdt^CW!NuG7!eD9CnjS~~(ikeW4=sN29j@F`wmbKKMthm?0E4M1zJs5qc{FYe`bd)l!EN4y#-$f()|C%k$Q}VO>#z9 z5y{qsh*usWIs@c8$xhEXk|^%*xT_5Kn{QqKIPslIi+6kIO^~4Gb+G8cF2m^ait}WmN0eDNxXkZG+x=OUIkgogCUG#Kp(fsjyNJ#?)k%@p zD<|}-_RLKHHfW*}e9g`C_OuwaKHW51pY5wq01h_#wmqdRVR|z9apZ+dorPt~Zbcp2 zFVrv4M|UYOdhs7!OD@aJI=hBEm9jRYNVW>EZEb8D`F2NZ!bo6B7>UZf%IKj_6aL^# zw~ag-bF|c~V=39k!o(fP_Ba-6MGP?}qKxS+IfBz!=fEjz<kh7{(sK1WFyW}x#{IYu#x=(vaZ6^FZK`EL z_MkKO|BN7s0yvZdu&iO|+ql;r6S_L}&IGL-Q(OR!gHmsWlp|}4DYQ5+)KiyPn@RU% zch*U2&dl#%3S3Nj5QAAUo+r4z3pQzI3_+Y0bcKndlGlg8}j{T ztujPd^T_MPLU_|$!sdEkw5sPC5{A@a)QeM)4~0ET(~<)hS11&txJSx;&bC`SPI8X8 z;}EM6o=O-$rD9QhW}Dbt6t9}{Sm!XAeZHl(NF@OX{SWy8R3J#&RT&^OcxnBF&6)4Ck8tpn%jl#RosWxGSx5n{4pZ&q%&5EMc6 z-Op^YOTBT`4O$34?MTzU68Ne;#Z?Y%z|D~VycJ>{GIXm27Czl29wf&em0;liAwODf z6xGM?qM#@N>_{e6d5V9UsLH#eLvbi@@LYfQ`aMX2g0+k_2w}Gmg5u9~bN^<5?+&(s zcD}fHPW~DS`TDzgh?BtMKNU_yN-gQv;D(v$%-i-SJp;k|q9NVxQisH@O$;lq`w93` z^nwx!R$9I0F!4Rxh@m{Bd>@|J&u#aAC{-OyQhaHX1ZC9UO5oTvV}h{~Av3u1MVO}B zMEQQ0PWo@jV74pKzzpTOSL;MnE{d0@B{C~6SDR^kvl?eF-+ZcbDA5;W1A=;Q`8&9& zbF(xS{Z2Q}Q-7Ak>-mXPR$fUUR89LtzEmV5 zWT>|~99n$%K=h`#O`_D7Lm&Wa++IwD1I+ zaF`vVaD)hEUT+YLyhxHShX+^VPTHEen(* z^Bew((nNKDvVh2rL6yxKe?QH1$&|zJuNWpE357N=R*dfIO8!CX)PTaZlT{WwD_oo6ssfi*p?$Kifz^iF7sV3(E)cZ>Z%kKfl)Uj7X$8} zIs%ho-gdGYABi0*CA!lZk=!4$`F|>6CRC%|TzD=Bh05098aKXWv3XsC*;`ro+qPfF zTyUae6aB$?Nv@XqcBRKQmQ)-R$GQTZ!>svqBVh<;V^}URTIikRI_=N9^iH?!F(2*G zSCb?Wd=|Z^*Kvw6cRQN;oYly&DxxW-&^%9sFq78WV37~DQcH-WN1)w}5CLr5crH@1 zB`!n?E&o-a202xAXwR_iuL;J5!mg^g+ zVF7aO(J;tB^k<0zCO_orjiS@?8`ZfALqU{egMOZN-eLcTVud`zMqV#|fV}05KP@^; z1Cr0lb`}0{vYj$H4o9K{cIaxYf~d_S?<+7xWYU%5_#OV>3B( z{nUpkl!Z5^GVuBx-wXtX;0l_#SWT<+s<>N$z(YBBBn?}}ueltAo6;~%+v?wb`lH1J z<&1E}5>D8QI8+F@xDfd(PU5p>-h!3y{(m~`8j$N-kjwL6Eyi&m$1Itr1ICPeYlJl- zpZ}5W$K1Sk>3uIu-pQq57+nTTxe|JesC^cA*>eIf@_o8<+cqko`wJlHjEkU4dxU+~ zFeZR7Ns`u{&7^+mY!)|X3o-(&X&=0~KnvC6LHSxe56X1YW(KGt34R})L89f3FMP&t zU%=}>C!>`(nJ(^nf@WRglV3b6hNv)0m7gu%%Rr!&+k`Ad;ce8EDW7$ZbfU@8xf}e>CmhM=64JL-i?@jr?wD;I)ZavEok4*7MC8(S{ z*DyVt*f!_a*<2<=a(t@Gg2&1Mh2ebLJ-r{MvoPj8$CyXnJr`zD)G!EN3}SS8KCZ72 zZHF86qG+M|x3_vIm(>w`u-jz*1@@+aW;ZVZ4s984VtT;C@#gRaW9%-!2h4B73(gTa z6XgEmBc}cr+$=Ndewg~`A@HzZ{>~*=mjJLZcYUuCc68Nr8n|t!v*MvU?cbbwHGUP9 zyrpiq(cI;!QSu|Xy>o#0~?Wq7|TE$gO)=d_m{liE<1b(Y53oHRsZO%+k@yFTN`~SY$JU^>i^96il_B z14>6{?WWkd?pOB%(TOjE=T}3FstNV=$v;k?#9ZFC(O&N-DDIdq-xS^mesBd8kK3on zk6lYAyS!eP3L-yCFViHB=yxvLh!S-H^rp8s_M^VmKqm;!RtJaNAMP&rrZ2qoPd8%I z-}WXOlaf6DjcNZ|>#U6PY1e>?ss}Wt9DgJ`}Y_3l`ipkHKRVy>zy-jf(|@WKRbe)K)nXWo6%MOu2KH535O! zJBP0Q5?g(YXOkcX5bPwvkG0J3N;r7hEv7fe)yBr>GpMTkx%2)yRiao)QAh!Hpd6uz z-vN$H?mb>9wd8Otb}DstT&)!u*4QYD1L#Dfu}7`a#*%&=_y2eoGE>3;}d>Uouz|_KX$xW_2Y|_D`0c( ztz(3uRQMDA%!n-1+YF~}E*kR8Xb=eJ?qxIZ;;KX>*q|sQw2B_Vx*v!atFnE&%`*JMx(D2j?czFqE4D%zfVP;L=v`@C6Qpfs`lZjL3*#Fb?c z>#t7&ynPRvPI_M8_9mNF+Mzv3mXXh#?K`9l{4qOv`{)S-0F8S@@WP`~dYqIGz1~cm z4jI6>h7`ZcY2wV%Nt#W0a0b)bj4D^^J@W$0r z>zQa_t~6%#h!hDnv%DV|;W*1MFP zzQ#HoQYXS*XxzccMu#=csl=t^7ZoXJhRSBNCu(WPSsdiMa2gBX?@hDhq!s^w;%?%*^`_(t=3;3 zP|PXo`=LqCZkPxPx)_4+-27J$+rW|F65D=UNl~ulp6ss7xAe1j%I!kY8${k~*-6w# z*yD>!o2I{$JKfKjWSe6`c5~KxReQ#$AcOQ2d;pJavfMu*lS^+~D>*UFh`=~Q=?`7= z$|8Ry>Q~)6@@|3z)=Fvi|2XP?OR_q>)+IWyUgeKiP5ST2GMCHV2ukbVk~-aPG*kGa z*I|ZT?#^PQ4U9m%`QiQsX_T!94IlD&9UrRJuFt2l)46vCLG1s4=vq9k@koV5% zaDv9DUtckoUc~?a_>S8GA{Kmg(iD_>@mXWv^n*hNy&C8-hCF6K6|0FoU9^fw|5a{9 z2PHm2$>^_hVdYbQ>#{eQO8Vq^>AlPKc<%t;aV_`()yaYt@9cVq%UraCbgn{32 z>E2`S`@v+8HD>%>jEe+sOKWW=QrcyB{%0y(;Vj?NA+b7MkErI$vYXh2FVv=$;cruNin}&e*^J4wpRONfRaB{{n3kOQSC_Ur_yyHe%zRerDwkd)0tcK7nYeL zR>R|t*Dp*v-iwbxA}_vrFzP%Pv>vJs5um!HWwOk=xu_~TvXns>cbosAW5vbei7NZF z$GU^X=PNCK-%gvj$hkL+o=yhA8x5T=n4qpOy?FEB#65%;S-AJj-=JcrN zMpF)^PM2A^Buw#lTUUD7g1Ab|(JVo+lV>mG7ZTs2x zsu5JByNH}H-oyQne#i0nJ8F5tSyouHcVi2{6YRo?{LUbuyTgerRxbDo7J3GrP357I znnEL}X)=z-GLm2UUTENLOwaCxYH?lPGVWGPh%Pz3rRxBfNZZarsUSd9p5+?GT3SLqdjLM27!Pb8r*X&7S!fqzQ;{m@8%!{Unm}8DQw%BR;Sc=80VQW z3g^HblzjW!{y&rEIENZ$1U|eqfxST|J65e;bz7|hU4)Mc{@%=_t$TETmG^M9D03Ms zbNSgb* zrf)>_Uwi7SIuU3qG|h9s5VUbqh+f4AMNqix$Iy6;GG=8_j(a)|SA{5zeU;3CdR|Bn zkn_|B8$UTtOM(%kC<*+Vy3fWmnGYysOqTDN*NI3L)6ueYUu`EaWFFkNmrsJHp1K<#>ebZ-`}$5_!T>9G(f0N@xP zLocZNtR<+yfy!r@;-j99G(%{y9QL0`Y48kc%;UuE< zW-w%uuSF|#bMM`GOj@JGJ{@Q{ysmUu&3^}>0WHwis@}>%&@gaaBfSA#jOcD%!-n|TB$nP97yZDwTX*H zQIkfx^L~|};F*fEt@f0~Elf%rMcRXt!=VMKlVK!UD!+2veSHt*4q(brpu9nF{?`a0 zu{Z;HHuW!Y?oj-gzQk+Cd=LZzZy$3@bJP)brmAODB)nREe=+w2r0<#+!6 zpoq_ZC+}y96^mF+~(ZmcmnFK1)7I4rOmKFPh?IY68jJm!V$sU3bH<3&4rK<-LVE=xEG8M4ZFruDy&I+xl7_5`_wN8Vb?uH zH0c@hAj4PjA=14e;lDQ{{`>YR2j@%DN}re{7u7O1K;pauEIA9N3vW>lUlvQ$z!Pm3 z-XOB2AzS~nG;G=dKp9zRV&O}Er61gQiN7y8AMPj?yAs~zA}`o;Y|R#h2tGW2T%L_Z zN?IU`5Z&sa$qYu8@><3hl#0f^xDoe4HnJp^LbwMy;xv0}LOJx=MK5*YNk$vWHP`Q?I_Yu?d7M zVz}-1C9dm{Y~PPCikp{(zU08T0$aAkf6CROX-ygb=77@^e_j|MP`~m`MvzADoW}^W zCI}w~z_#b^T9E~st128*0{;APttg_6KMHI0nROD}sC|W%q!z^Tr9W7-E@k}@_RsY$ zN2Tu3=ro%x(dBnN#`={;9vL*D6J5RA=I1qO{*OKibcZ;;SwMNCG;*o13jP%U*_-@E-Xz+=iI|n!TIFqj>XGpth==V;h5(I!Hg^?TB zijl<2$eNM&HO#vgns?$Rai)^qmDEo?b(kMz$2~3e!rC?6QpV?Yqv;KbO4Hak9QXZh zULJBy+0JLY$&OXFXpU($k8a!7ZrUHxZUEq%G2Wr&H^>iIl*~{OrYcU_Njn+*t4nhK z&tFB7;+IBYjnAL#8luFr)6`OiePYS0Wp;h=>ij!JjtWK4_hTQ-dJ;6iIl>Rd9!EhE zpJg}&q|#NYM>F0>3SYEduRhy-_-$$2q&!CCl68_y{QQ!4KF=c%(H7))X5xRjt6VMBZLE~)$@%+!X<{Ay^WW!ww4*79dvqTF8*i|k06%tU*xPqdnVtzX1F7Trxca5!K% zEi>vx&5WNGMtJrLGf^)^nTkDa?346?4|%v0bC(nE!5!y5`$v1mu?^5*M^Xm-38ug8 zYh6A2oR`j-HVA80qfG4<@RT*DtU5eOk$8qMG(M+m)wqiq{D4ctoOIDEe>YXO_*xKN z(eC$TSg+TudQ;YTeIR?Az=dfxVb;L84#d5E*RgWJB+X{vk4f64F~yt z=P&X(tqFWtX0@iX+B0|mY-^R$0m&$#_Aqs_z@) zl%6Q-3R#Gg_v5%B(tteF*8V9&7*a6`CdZ1-&l%qQ&=kkWHe>S!VDb$AJ+r@g2-u zgsT}!NjpBLYZcb@FtYSj5G)v85{-TTv3upvC)24mLha@$5CK|XpUP+y_4Uy@_p@HBG5$J}$lhOKR#qRKnx@FB~WPb10j z(Va}g&cpqg-%}!5l38fv^FZO6TNNucC=L zVUV`2y(U-^q$IVoO{G15*ZR948sB(lW?RYBczY}w1trC*;vK{DoaceNdTe0--ALlT z+#5rQG1X37;R^^uzOa)eB6qFvF&tEel%*&MypXc{SEEhm(d;0Fn>_H8DBgnhk4vXx zI%#*8Lavt6yVn`%G3oHcqniO0>5m5@AmAv!BGAcAwg6mkoj*RDRGP-h^WcCJNr`rg zeb-a>A%Q)*#|q2iZfoC7Oc7)VAhrm}WYbx)-Lq(5`i@ki54f~QJBW~MQ%&V29rjIT z(MgXU|E>cf{7!wvGt)C5S_^9&c5jv)b_%-KeeiCSPOXIM zc$vG63U^*&R4aej0V#YwshPNlehZw?{|s64c&XJaqt5P|CK8>N8k&1?E>^tq&{P~8 zsX&(+OI*a#t?#+oNp2~I1WA(6kT|H*KuJM!cWSG>{^Q5Crd?v}9F{bv7-)O%u$^Ie z%jR@{_nCTOGm?vWdJtON(PsRtNv||RWOoc^2+-m%>(qQoqm;1 zo#7cr(LrxgYpd?ey(!Z8JhCoUF1yr|l`DeS9Z^Qy~v3LZtJTHI@s? z2r$a5UZU)w_9Sej+N%ExZ_+zD$aGt^E{pVj(azbN`4N8dFONe$wj{;r>S@u0GBwuh zU`v7-@!5-VH4wzqbG6DhZ#aS3C$ndLd3DQ(u*W`TM2&NH8!kF-cC&U;X1#i4Yi%@x zSM8JK9IweGX36oK=dlF?tv)wd;)20dSb~Q5x0!aODF=H|Vy7>PkEI^2#@EOWirtSY(z zi1?Fve%ru0H*EZwaC35i!0>#KL@+`!TQyI{^DiBZ?R$T9kSplW!vEXpP7gG-N9++2 zCCf6`vigFpX!owP%DhwB|4-Kw-6+Gt^Ub!#Wmlq;Q+Snc%7jK1+w4q$DC}VIE-C@ZB#dlka|^%mxIO@bXkR2lMAPYif`Lt(yF1 z`=>9#xxUid!h=F1~a3e%xmV|#uV_43Lcf^q1cf%YEqA^l+pPa zMjzxpZlxu>bhFw+nP%dJVrDUK=aTrCBFH9hFf5d>lO ziC44&4MUn3j+1>MWpUb~RWHC#g7(}c*!wTruCoY(mNlFT3)z^_QK-n?ouaX8jEofW zHOOhIo$Pt({gHA68d+LdF0>04f^lT3lj8#HS&?z3Tk{?oE+2#TeRAE##N)o4UUP{( z**RKgP~>Ju6FV}xP-Rd;P-LWNVu3r?lGVVdwUhC!r zcZ6VP*US{WSD~81^BBfAz!MC#6$T<3s4AkOF>w{qB?u9MZBs{v(Y27}2r_GfJPycO z-cvH4cAW)73U!q}g=qnaNm+vi$`c~_ZQ?TL2FRVw+>ZYfc793MBfU>#!Pv+2J4Bd6 zVNmFnr!$bfngUu;%tn%o%t^?%r3J$CF3Qpgh+Hv+V{0n8VSQo7zgV1#s4p$h zj2m~YiTIhFD)Cv3HHwAelQ|#i(su_7MLuv|+#x(BlSRhy(??dtSV6WMRAuh*vh_;> z>=^=0vI@1&pD;3u=eZZyDk2c3HL)lPs0I0uDSXhA`&Ksk`#3>H zr8&c$8d#SlU!1M~MBV@TJzfFX)V?C~ln55>_Y{3I$j3|gJl`NfSuKw|S$^7(!}-N1 z(}4P-Y{C2G#h@cLyo7H+05Lx;E_DI>*33+6fGKG~S02=ucw6@9&kyf*dI_;QW!%>H%lL$d;eE^doeV)69sk z%he9k#YLdk8BeseuVb`cMG)>RF;69c*%UtEb-eJGroCR}`Ed)*Ot3T)u#vU>Tf7yr zL3U+TPB*DeD;*-FD_5(D$zuT}C$+45=k|XB#B8(@(3S#|90crrDAX4d3Ef2CC*C1w zenugFhHs|N&K9Zn0q7O&)1emqZ4kr`2*`<$Rks{2WUrPNR7+?TC18P_EE7w4pM21Q zM{#A~003oZAA)%PS1C}>5W{x?{3^_NR9gww4tQrTUqo4i5F!`a1+W|T_zSFF{~G78 z^|KKONBxJ(_9fUhv6N1t2~(>(Bo3PjTP*ts`KL~0u{Wu{-#zSoeoT-ouGnn?ZyuvW z;Fg_#Q!D}fix;S6XDoaO^)SS9EYLaT5W?$0gs55)9%%JO{SVE7RSneP4xJI4D_{&8^&EP7eiq zS~>LZ?Tt`e#AA5#8p929z5fmbgtz{emY|5NyZqqKJ3&yS9nF7cJ#>#-kani9#r1hH zKuOU^QExP&7C(t)8WW@VEK>;v>PE!yHoZ=AKMuNf?ksmiNEDPD9c+V7k-kFyigS>r z0uc9jz4q-eYcqATZ_2wK;b~hB-h-D*-vnsc-0(!6vtw8u#WN5do%fF*>2^IL2bXPG zr{M>CooZU<_lS*ubVqu{38_kDoQ*tiZ=)0MSd?OshC| zFO0kZBut;fO=4mzDETvl%w+Q!$kcTlDEHr-)v^hTYNHIM!fW;{K+pCIf3bioqUlrE|DF%^+dgzLg+)aT z5%8`T2xfJ-?k0tsAK)F}UX>y3shBNt(T4WVe)4U(ll#uTn$+w6`r7$wY4yCp2k=i` z>y8&xw);;vrnoC}CtHqs7pyo*9eU%_YR?aS@^aia3Bt?OfZl_ z1etuut-GIc$o>le*U7a3<5`R6;oA&$w1KbjaXY{{7}HGwYwe0i^s*U3*z# zy+o(_wX@u(1%bJ^nav`uMZs_&N(u|4`BDxKTmD{jfCtIB4OJ9 z4u0mG=kv4VT{1FFD^w~bhvz!J>wQuuAM)J9pmr`Q<79&8TZ&C~ym!OO7r(>=?z~g* zyoPBEx7%)dW^)$`X3k6_TtDnzVcmAB?(92_5z+S;23;oDl0J7~@tqU>kpN0t<%k16O{@sus_vpuAl9212aVGpyKygn-dN`I+X~M5)3@%HmE$oNRAN~0 zus@NS?+b{YnY#KQwqu!hCEc&S_B@+KY1hVXMb0c9R~IZc8~i6z;j6{>l?nb+LlO0S zNimX6ZT+cZ)>fPs%E8UiF7RB~wf*jViS{ZiUn0K+^fUv@V%2QR=VSd@foE+iHMdac zQ`P5X;}?4oFXQjE|Nc zLAq;WbjTel_fV3!$gwid|BsaRGMvan|RzkW)Hz=J-3JARO6YRI0J?Gr#AJO0rc{c$E5el`%Dd?L(i;^+W=R z=GohipUmsFB{H^OWSnZ+E~i!oigmn6`X_b$SIUcKnFseHXx)LjH&Ex!wlFfT+8BB` zxbLDXZLb}NZa@oKkWVC zTXeZ$<$Ig&J1|`wS!_iUxYjuePMT*u&&>8qhacAG{)=}m=RZ2XvI3i~I(}|02~85)1mFi>sA>W9lhwzsiPoC*IXiyoU3$(>dBX>N z(^-v8XI2ZcvkjQp49PfIg>HB$Mm3ecJbx@-+I&x%uv~k`!9+-99+}L#|B2O?g;CY- zF;`mG7&OTfn$*16Ys2&u^B$Wm<}_^YCFiB=g-7NXBYK{?O!VVtT!HCaSE4-~`wtp(MdzHkwGP1x4*ajIGA4!4KB-zfY zN)1rizcKa6j#y1c(@s|{XsN4x>M3j4V_Rsngu91-(aLz(MGR5$(u7V?*mpPPDXM>b zt#sNcoHq6L`S#AKC4R>GhT`G)xJBkAMLHBbqS$G0@Hr*`99c2mROa)QVpd+}n{h*HpSH8&&jBPO&GA2KPS6$Qxa4D)x^!9p>W@&X%GLgE zbU|=AGMe;o`osG^6H~Ddyo6?OXbX`T7j%U7AC5;&C+MD{!Ep~G~7>D2Z^@Te^rl7kBLrJ(bApy_3k4E=DPG5oM&0ahsafLQs^9#PLwN~w{N+K(pEFtum|L!5H z-eL7b~w$Y(;{I^<8hgVXM`rT&DxAc*Ww;zDKQT9c>u3uJOm*fx%Je>+-k)z?4G zBowD$40n~ldi|6V5C0l2QHZ+Xn> zvQ8c#XfjU`?E`O%EB5lJMO_I(H3BlbYKGi#>%(h3-=+y6J-5H?SG8uhQ*>b)w68{? zhOs)%Qis_?$4L$I$A3{x>)xg4Kfo!2gHgX@8jLD0pE6P^af6Dp(cHf)24{pX&C-(Ys`DKAc0Wf*EFT+38)he}CY zz}hPT(BXWn8|CO9SWP|fvNn=Teel&He=^oK=nmbqaGKJ2zLaLbfN7K;2K%)$DQ9lw zxQ=Q*udu=`)^t8?O<{~0@n?7?JG&WaXSue?bhKK}PnruVw=%tJx@{-rO!9KJ(C%CG z2E7yv2)66?mjXgEj~{+a<;0Y8Cb{j(q)T%Jci55&2J>GzIeNXFyZ9GtAcCQdBh4vO z1b7%xy1?&inSV-Iwd@$qkJY{V!bgh#zKd}LvR4% z<3ZZb1LF}Lw}Xnf$2o*AEiAf>^js&yq3yvTt%6Bx`iz5JdkS)6v-#UEcZeDj(vuLr zOQ-Fb;#ExDl0Eib`op5n`F#^C@P>hsEK}a4ll@cbHSv(E;^!A#M1n6AN2tffy??#C z?#wRMOuQzS)#UU$mw9Vjb>A&`rq@{=IPQ$DS|NT1z=?|Z=^oZK{(aC78DAYERjy!L zoDC&aetG4b#8gDdxcciqjzVg#y?&%}mfVCy?PwJNd@Dzx#gM0Mc7=<|ylvJ_8n*j7 z>y4JAY<6s$N&oavxZCc3tP_a=y1QsJzP1c_|4SmFL+AiG7rsF^8-f~P-@*FF8tWl^ zic-jo3TxXSjD>VN`~G-xIfn2U_@NgFevButWir0KP@U5U6%HK&%E}3&LfVCAQ<>j> zu-i^4q`fBQu@6n$NJc5MVG=bs0eZZ&uNF_N(U*3Per@(V9o zOgQR89*Il0w05Y%N%Q4`^Tkd5;mwO8NalSWr=klcd^j^3I6`I=6uz?NNiCE9IgAj0 z?vriU!}SC~BsC!jzZDNOQA<9greJ=i$jJ7D6H z9H>Fh?_{bpOCVlQY;6EocwjAy+5Umi-SyT7CBf1wtZJ<@n@aR3zxm_;^vtr6e$BAz zN%pUgYJoTCvtjbXKYGdtNjJUE7%=MosqO~|8_8zble&~-;>%^NdwP8zp!N9AvFuj* z8)nK?BR^5s{M%I#(Qkf|rg#zjo?>e5BqY8+-+ghdWBBP>oWsdjv>i95?2+-<_*bi! zbpB#S+|Ff`O&+$5>^~98;=wv-fHHuri;JKMx_VD>n4{M%B_y#Q_(5W9nq!AD{ z9iZ;BEGT7zE3{zA6sUVBvip~3*lPH@=aRY!|6Q=*A7YWJa6 z*WXfrECzp}d#+i>Ph&EhsG%m4teWXKcx+_k%?Qalsq!&uVo~V#O=_3tR{d3uy2Nyi zskm!{$FMs1Ml&dE@5#da=p<>rT7~_Oq={4c8jT>Mi)~zv6h?QHE>^3<;Ha&^C=ET? zMzdP5xygc@d;HRSOpmD;!V8z0aPLm7G307ylOnZmAJ!(;BxN`KwlCZENhgv@Xw?qERSz) z|3x@g3&*;A@v>&$jkAbTsP!~8d14ZQ@xUjbvHGu99B+rz%Q?vK9A|p)d7We7Y5}o1 zPDHOW7{B4Y)8pF~M7&{W8+glZeUsU-BKxxDWjQ-d>D$FgorCiYW@MupN6Npzg1Xzo zF1nBtV+(qvYY_sGR=JtLd^`4R7OJMeOJPA|EJHgY$MRObx|(i;dN)b%A*h1HqsKWn_4KW_cHgaL?XqtyT(_Z-&y!E z3P_&B$!klq(~>0DrSX8AWNcYZYwIc{30&t$)0myB1KJ>DY#qXP6r=8H8`Yq)8u-Tl zADy^+`4=mJ?Yn@d>+b^{&KPoJ3&`V!EW$+wqnd1nsJ(-jJSAIW=9P8HULQj$%fmT4 z?uwG!N;UV09!ZhPzE=J{4$>BU=%Du|{lhnuPFQsSG7>UaGaU26Ls9h%O5o9`T0o(i zzduvyo{9Tuvsg0=uc~DatqV`5VwEoGm>q)BSCg>)%;wde2hc6=1;e0UZN0y7ge zvKz4u3TMshiEF*#SyBgyJ<}XzkMKSGiK*H|I_8#Qt=hj|5=;N^2p*6kIOLZ~o9dnm znnxSU9?6f|ke3zR3qNqS#cgn>hd+q+0Qm1%JLj{(M-5c~uWAaeeo^BmkCIz${d*yh zL!Oq#*$3OpVwyeo4YvbOtGd%49ix61`2rdMzpb)1i}8HU_Po{2b^%N~$i1=6xuPxp zM!h60f++bSr+4{nmbo`e+cH%*w;hauJJa{~SO17$!DPiF#O+wTkTmtlUx0IHHvj{u z)yV7#jl9`*PJhqZp2Fa}p(@Ni6#OSD_+_==M%_81HpU@GOZ#m5rC{BE*XG}p1Fu~# z_EbDdf?(b|J7-l`BU#^j|9&$Xw#ce)lqJHKW=j`~5puPt%{}}1b>r}b3xoBbWDbtQ zfX-s=TiSBrY@Kp4hPRH6%ZU(q3vy{%ICVtLuvo%U0g7apez<6{@v&{AGQ*3`@O}Z{U3nE@#O%whd?dHDfjONr{L`I zF9iv|_5QObu0Gw)72~u1)fV2XUF;;Pn3d)NQaE%n)cWDNn0FWI*{;0p{l->!3h%H* z22fk_7f5o?zq#XJOqct>27HUtH~HZ9{&KJH@0$h&v(tZ9rL}3=a*bS@yOrIVJ;HE5_juuT;FeWi*iBijp+>L+i3_8){a@YR}h`8<%9!-#w zQsRjvKDAQsd8_h%J%UC+Pa(@SBY`AJ1B+^z`^rdF9aK&9P67BW6a-saeBwkbe7~b2 zdtKtUl&c1*cEk_56|MFi2@5e4+ETz3GOsFBShe%!?)~5G+(N-) ze~;B|=)=8SmczN4pVieJ*Ly5hrW5}2aH7txUT%8TT8$zyp0x4b{@MzhA(+%O2){#^ zfsar^?|A->Tl>4k6lhkNO9&Ykk6A+mnN#}9_YrRrq`h=NX^yP{6=apY*i_s`yygA5 zrRB#3`Ob1(^_lTu3wjQFd|{>thlj>9@=gilI{|Nto{HR&6)&`7Lm@LSE}O^Ny#)NX znMuw%g6|x({TKFCCkG_&I6Jy@ap8DTvFa@<&jla-~03@E0n&L917@ckz_e#%Jr-1DITYo#_e zo&K{|%*~I9>>R|&`PB&UPmJL&FTP##0nd#L-ZSYAc(SA4ZOeG%LE^f4pEC1UsO2Jt z{J?tH;55(bR5z)t#-p@HzqqZ{LR=e@j!t8+&(BWo0U2{_T)A_ErtR;hVCM=nx$)~2 zmoVvINUVo5FoQW6O8WYSveVgYNt=8MX1xrZO1nb*!&<11N$yvuNAn}2!xUr>NO$vo zh6OC~WVDDy>k|moEM#7MQ$%mQwdPsQ8#XE)jewLX=L@>E{K0>tuo!|J|37`jr!;uZ zW7g&t_Gi)R)-9^=p`VBRfJY75YI|Q?jGM|ex&U?2QS8>-)HEAdx;1`=NC?=#-l=8p z-jM6!!`l8G#_HU_i+O)*zMg00JW0h7o4AWxQaLc&!hk*`V?WsH-a05)Trm2|- zO;d2Ih3iZM0>VjZAk}{R?{Yg=2jA|`?mDUteA6uTCWv8^B2SVK&1;Mh(WJMTFZa*~ z>Y{YE{^{NNu1f=I1wMPDlz$EZ=arg?C7N?`I1N19(tRB~Atc=JqsdEmF67QcRC!g4 zqhOg>S&PE89G+oM;17SD`DPa#>q-3xpf1VJjFinXi>d=^$^i z-+iY`!_smj1W@DtzgxH+`VTVMR2SJj*TNy0VgqXIk1-Mv08eV2{Tw*hK%h~Fm#;kM z`U)$wUK>B{AQw{Ikv1*g`*mDyeXZR8A;Anj?krqJ`OAt|wWZ=ksXKp9@B58~Ag7s% zcfB>0zZ$FnT;PrIxZE`_q;yV{7&et4;VpkuNr;Qn0Q_BvgY*?!JjR@$~@=QjzFp2mdC1Wf3W)c zk4roEm8IR#m1^~st5I(guF#)P#(;F&b9@dOf{PvKWrBnJj1I|do#dk9(+nP+pC-0! z#Dm9hJ$t7-mYXO8gEKZu`R~3v3M94+N_*DfZ`lNt^Bg+|UbwUA;fA+h+w}x>Up59X zKa@Bb#7!W1QE2ilMkP-L#dCgq3hun`4bP|pEeVoKT%1Iac97__jTa zy<M z*-)DwTeq%6;Z%3U#Y4z`{`z-E3QK+lY?gL-sC#@J&^9j>wJ`IF$AUa*6#zYO>YYBV0kV!;3HwHY9hbD3; zHD;;iv9jxIafIx(rk-6~Ae+8aU?fI?H|GI`EPvM?1AUb>!aZ)U&;1QRL~t+6z|~7< z8eFw+1dF5&FanCoC^Ga=&J(!aJiPvcp`m7Hr!1D(DcNF#3?zjv2w6U9T}}2Fi$fB- z>gAtWA^fUq6`VDCPV`MCgB3lW@2()HE`n7D#{z0JKpx}25B`1P*o5oFHK**=Krv3r z2MxuzoiFT9ghRSW(8l;N&Lj)M02^D?AI5An{HM?YebU?TD7$#HYvBSoEA?Yl4JypgWpOsx-t1rx!e!F!`j$LC?8B;zJG2D z{P-NUbEBtgaHy9Z-XCCnC96GqirDATmAJ-@p<@{ zyiuhT^;R#+CrB>m^^?k`l}xSg7ZP+}{=v}XDAg|`{S+cea=MjPYF|k{KAGZ786*6p z0%7E-m)&3a$JWZ#$#)!}pZhLOZA*y#!ACE{OlG6cZm}8747(i9nR761sq1wJth!SH zb13VkXN$pxhk4*h;7LR+`@SWgs&5UG5%kONT zl`Vzw2(r(9Ko9TFS_Vu1MMm!8g4O`t8e|Uo3=XtEkV!hi3YrWkk%8tvg03B9L^aX8zq#nZ+asHL!x%v(LBIA59nuQT5gpB*c0;v%Sf zeun1@D5G7B(2W~O{2w&4#8T3eH9C1!tg4nWLS;VoVgqV>G#00svwX@-3XxPFrtsj` z>AKsh*wK)&OQbaOu?F)aGSD~1fs$pTIKILX0WgoFn z;lsNxdR$bW(Gg<+kAD6hrIxvI&Je0`9>|-8iwOC~NP!xZT)g_!UtRuCeZ~B>MoA9z zGVlUCe>qM1twfK-;QVcH6zp=RzDaQ}45W&S)G*9u-vxHkYiEOlj{d3`$ygNQp4nk@ z%9o$)EynRQqVQc)4@%4xUV#Bm&WD=k#`&0%7^7F;!NbG;0a^1jVY&R+1FpToUx#X& zHw%?X75;>6j9a#08>JcmB_^BQ`}8QGLg>e*rTmMOYK2Ll@c|Au`~;`Qk^UqcIa(r~ zHyBP;u73K-vamFOQ*#p%E-u)`Zo2^E( z_Yz5+{|4Y(1aDT`+>gxm+c)ru=AaefuqbuC>Xa9%#qrQS4p5<5!h+mQU%x1;)*|Oy z^{L`aJ{iMeq6N~N0Y*-N?0EdbGc0o3n=`{f1?WxIm`FWEetz1u$KN=L|BW%`={KzE zDI(d5mK+mW3ckM`!!lM@uZS9=W)MFbT~`H$PgBf?l^s!$?1RGW??kK@Vku*wpHm(Z zDoAl*LI zHG|FyS#HfPErF9CLZn=gDzfkMk(bdP$ttM2H*%ldN?*^OpSfwKCJMZJf;nvs26bEd z+kJnix*|hgpQ7#C=+S;x-eI+uU!2_}yH23}h5SauOOLk>@1K#N{T(}BkTN)MA@DL{ z!GZM%$I`(DYa&NtWy!qLTPJwBZmIM7J^5thAORp0@a|OgL@WrvN zzxm1-`|^UvJT2C$1q$2qocBC8z&2orsAzaMt!6dEKXrwcSvK}~ob3}C?N)qpW?8g+ z(28{zeK)x$l?&vKmtVJUa_^br{o(kGqkl|qYXb0uWNJT3j~85UKI&6V{MN5pSnBb+ zRBysG=ubC`%p%Qte7gLOuurN?uAO|iRSpJg6hU}UbQO$H6E#O^> zGHw(JMXz%oE;UagczW4kkIVoT3=XhjvKyC9gNx->V{`+_ZYgoRxrfPn_JKA5o9$=G zGo*4zo8}58edDI-hBpOvv>n*)K_RD0Pwan(Q5V7f zjo8`FY*l-2x)`eNbBO#EPOO|*-cda-^t4ig!s~vm53IM3yh_30PE#(b+8_wzc#u)7 z)n48*ULz_aSX6>R{w!)dj2HUh(H=-Gu(wb<^_avjK4sD}J=A~L%()P@wk~09M8z?a zMAw*NOYX2!!(bNssHNDJ!A2sQ&Qtjn6L)e|`^5q$5}|_Xsv?3RV%^^qj1y(pbU5_a(UO__>CJ8c*eqjg#GtUkqEuE#_Z~Nj+l@1)Y9?N*UzO#OmY*Z&%v~p!&OCQ1gVDdX)C8H8{ zj!6ypR_0M8?+nr<9%`}`L2 zwXfbb9s<5#mM@sg(@`l@V7MEPk`V!rkvTtDWg*G+Uh%y-zj=uYS37u5rK-Ap#86xR z;R-;g5BF=}@IP7vskWQ)Xwg?<`Hp0CEWQLd^?C^FZ?CxpEkIyNE{FR!K4#tNXn^jd+nT@1qc4=}&W55DgTiQYh>8sXRZ%-Us3XpD_51a1g zsGkbTp)LtaFo`BUoLoJWJK_83EmGXVKdq$nhMV7ltw?F8XlnitrMt<=WWyp@^hONo zdt1%xAn*=lVsY!kT`8qL$g6FLF|Xp}qPbuDlncF<4Ho;(%!H4EPhISil9WHJ$dz<> zB$eB&h_o9d3+ce&&%Ageu>NSz-114}W{n8h;{r8IMzRJ&v(>s7Aue?>enN}4 z?%IBsq=GPAhS0pN5^OS2@akW|Ja`&MJ4b#DTjGBU*~2GC>OZIihJfV@o|e0)j{x7* zUW^0638+|g7~zT0h!>vhmU}xpT<-j81#|LIRc$AOEn3)h2UTB(^Cm5(vsn>dliED+ znM4iUZG}9Il^NMh+PP+9_MYvmja#~g5NvP5fB7C07sho|BRLP>B z1Y?-pE6ls^M4LK~TI%=t%2rm{&T&A8GN5t9hTLD3A2}bknu_P1LFnC2P zLC#T~MyIg#meN)P%ssVb<*a))nI-028>q@121Q#}2c9Fzau3UADg`Sq~_%F$O(js$rz9#C>W^X}?W*l~VE|xk?mCH|2PV!1c zgGogNvwBsfho1`%qAM-54G1ga=2Z>m7u`o$)ceYMf4nv~Xy2`RjB4A0Ybu)WNwFX% z9!oO3Kj8@JMrKv}W!K{i)c&{D%qzc2X-I!cUqJ_)9G>sr3VH1M8p-9(J09m<)k}{d z1M6WY;W1=%W4KIKS~@WN8@fpDebZ`CD-qnvWpi5RcjordHyz3aq&FAgdMQxe9;tF$5Ze+Z90n?NV8e9Wc&ke9 zA|LH*C!V~&b@5MynjdD*sW|}+$`W3QA9{F=Rh6_CNWd?o0b#j;VZ^Jy#wfnW%rq?r zy#lcWqoFO-smQ&OG~vFF?PWnwnssN?o0ZRR+%ve7Pdrt+-zr;>doGTvnhtYa z=qf+nsl>v)@R(Y9vRe)br&|AAz}epw&z1^1K!L2r@zK1ZaI%5qTW#rc*mgMNy}y)7 zT@NwVTCjS5MWOuy0bxqCe;g+Gk#oQJ)<<61)V}AfLLRq!P7`vwv}3H*fwfthxs{JX zn`iPvy7;NB2b@7CMX+3cEjM{;3!FSs?}F+i{CXOcvRz!@Om%zkL_e5#-Qz*)~qr20isz^iL zox(o`(y+lcBS#_ESw8%~rsHuQ)C4q-td1mgHAU8b52mit-9Kb;QELIhY(i0J z96;8qK*|34<8_>?70YF|&bvs?fsgt#P}S+?^KXHA{vKJxE-ih&o2eMG+ds-F_S5m# zZL?z1M$WU;G=QXGQ?Yf|Vzq*S-*ytmmb5)>OoFTo%`=Ghd!TTNWJLUPZ`A`73BdJ zrYtT&z@x7z2cOd<`vBP?ZgK$jUjNe;?R;$M5W}@+bx@wjrB^DZONDDdgRU0Xqd$63fa>>hk7s>vA$Cr-(BoJ9&l(;HSwaC>C- zrds&kF!Y|c!gw@!5ej%C2i{-uTo)TF-53PV7aDrGt6x_$Ibwl-jlM5wyl2gQucrr{)jvCvUKgZ+G4jnlY$^LMc8#iF$6DX zSAhZ^kldkHe6G|&i@LD>u#Snosh|C`38#pT%b2tFJ{qgRr05rZ_j$2Xk~R|$QWVOI zgeIT^RTg0y=i5U}NSI`H9b!25C8pwB=ErM2b>7U92O5uWlKC#J#b9YT@YVEAvl1Jm z;jDTsz3CQ0xa=JTId>p8Nnpr7oaz1i+dF?9{uvgY0IFU;5~5W1++*{Kg+BsdCQY~F%If%4Z+kN>2Tk$ zW8;=Dr`w)tO1@y7mQH|{xqo{Jj^rqnZS0MjK*=!Z0WJQV8-;Z(&y33cjP2N;tyTeSrVsS0LWXJ2=p@YJ$Uoie)Wm>m!?+M? zitwotBKF)Wsrq?|ILHXITFqg8K3n!jWvv&#d%m5HN(rVuk4e|>Ct~3Hll?PZXP1Gl zbnin_OHn&^Uyvn-e)JxcCt+vK+rr4$54&4pXj#u_w+?1lpw5F{XG+bq&_=fhn3V0{ zdmBu;PkXse-s-1OKQ(X`p1PFg5oivJXxSL>Sm)ukl6A%zUV(qL9iZ(mRvB&``|Qo? zbW0rB@j*@Nm<8>*%(!*g+iEVLddTN6>M=`J_jpAs^M^65B&mF~TI{GhO-i*GNaio- zyxE)KwUX|3qddlJlK~lcSt8( zCPU#xW4l0ip?U3Fc)YPeoM|0I5}YcX#@>piHwcC8ig2qsymPmxmcIB*{w{(2lv+H2 z4TMz>N_T&;8VF_IrmQ{#Z+=SN=0_OL@`+9hnCM0)Kh=1JV0T6UoU6MFf8-`)zOxDx`b6Bb2G`f7P~jj+2B9gH4n05BN`8a zhfG(%JQKX)uaCNhEnHmSlLHrcZjthUUIGfu1hh|-4PniufWiKZR8gyna~@`wrz#f2 zphjX=8I)lIY8^y^(2a+VepM!`E;QGACNDzVAgQRpR`%WO>c_-!pia)ug&3j6Hti8P z&KL3~R5uUAGaLqtJ`mib2FMB*vOP;N8>lC?I^Y8S7rPt##mBg!OTOm-C*+43O0?@0 z%z0n$!Ajuea+zAeJdxw5@>YWr+q3695m?vjyrlHcn@eSX>^V@$s~I{$45bqcQ~lC` zb3eIJsrS)`k2ID0(IhNKgbBnTHV=4xA9#K=@=)~ny-dWBNU2|Tw75U2o6#RHInH_B zp^|fEY7!8eoLZ5J!TcN`TyOKW@qnr^d;(Rn>Uy6KY7+nbeJIzU-h=ZY27gu2bk7;3 z`RLnj*GtKtU*CvRCiu$JIvW@Gz5TDzxfrFJQm~QfRK>j%IdY*S=%>3bX7S6m59>lX zV~DY1rK8^xma3g~#~eoT@s4`^JA+futsmWZ0D?hJ#h@sZCK zi2xU&ZdN<@bU16-U8QnT<}o`q_0RYnJ}cyb9h5D`{8vsl7M!a2mz2}h>V9%`^~v77 z6Bm`*cD=uFrEvi+70);Pt-e3+M9~!w+8^bF>J7EF`d_-7ylipbGSZ533bf8By= zQ`y6Xef9}txsQo*wdD7oyjn;@ocHy^%J`}>vsIIc07Pb=#~QoT8;A+dIJcD0Jg1eL zk^Oh`+`v_NsdaYZxM+LVS2r+%jHk4hZE1qCYd=WxUjvo)AP*Xz(QdW%^nBU^O)$88 zVZ$oEx>2kWEjK^+U)$amc>O6W^D5B;ESGbP1f@7gs)vEk<~|%;Z!*s}1y&Oum#bD9 zrq*K$^_!-Wv^I2iGj~kXKpDXo?kM-epX}V?sij)w-Qa+mOVlx1eBhU~EOKwgAJx(6 zs$o+BerfK8piTO)zv-e%tvPb9VGGQT`!6)-iNyifCbr2|qrr4=dpcZrz&y*yqfXO{`#+1dms0lt|GQ4o{Sbld zl}E7eAeDkQ?o~Yz<7g58f|az|dO|m35q`M)8p~-cUMlmm7Z6)~t3T@L|2;o9ma1i{ zNicDg`#`B*-rM0JhLcsLh8orScuW0L+FI4D3{*`c+Y%{fO$5lHJ>5*D};qI&*p8b-zrRY@@j(HUHG< zLE7G0DPiU2@IzDYDb)(J7p}uVMCTMiwejtr)X0u-TpInutuD*14SC_r`VLB3M~YT1 z28Rp?nuY}462s0{pjY-~a~J&|%exI$&d=lkukpyVxTmxq1C!SK7lvz~s`U@yJnLS6 zL_!b{V7z1ao}#+0%8`*E!uA$u@~KQuw2R^L?{h?OfQoU@Y05JA{)wkaSJ&Yd6|iF?`@WblZhq%@Ng96rE(&0wrbfa+X}vE)6BI$ zV5tMYO4?4Aus>swF=ri@jXE%sZ3)m6%0(%ytzxpHo=tm=k+2m(OnE z#hy752bmNix6v}~dsi|D`!ASr>sNxe$onz24Q_8s+uB%@HB8Qiv~pHN$A4Yi)_6a> zSi|jP>gIYgpT%K0L0J)tZrQ%SV=R|E7IVB;jK7SKG7~oa{w8wMUN6sc z`)5h1^Y{SJ-$&C`f`k+AiH|7en_Zt1=WdeFt(>P{oTG?E+KtB(1q8MZJ5lE)whPdHU)cvSa1d`xEULJ zAt}EmvcAeE4J>tAjC{h7nffw|XRSf~&g?BJ@rzoqC(zx(@Bqd4Gk@rGxyd#6bbLDS z`kX0U@{Km6hfiJr_N-hXbWm4yih%mU{HNlN$+wM+QsO1xdO^e$)6I9N2vC9!D3=xr z{oZAHWy_Lklp%RN;ui((McxB@LJ?v9tnFSEP~v5os0~#L`}1`(5p%c_do|g zFK&oCq@QxbX)BBxH-1|bBwy2h<1=e2*losZI`nWfON(A%`HAiw-bXA%YfHamVNgkX zE-1Tx8@qQWK9mF6^&POGpnf9D7=8}LS&*M`KDAwe5GyGFW?ZZHYaYo zE=W`jMGIQhUeA3SB~LW_BoQ%Kg8<}Y=1grbd7>=?&p;_qEIHo{yAw)L*x_qJw7sJB z{3@Tu)0Zr8$Jy+-4?(j0+GeiY?ybDIJkL{erqVT6xJwO>r#u@{W+YK|zVAd+5$(9| z@26+?g3Mcjo5+JpWUKUf`YKW!??0%^N4KZAH=h>0iC1axIPi9ZnEb*Fq2B*62B)lj z0t_Q)@jG+min44nGY2|&%2oEZpIEeh^}w*d$s;Licmo8rhC_x4b(&S23roFJ6^^Hx z!@t@;)&VzEb*UCe8k`p~B@1~5(_=UNy(SJJ6e6Ms*Qm@D$y+#I7i^bosJA_>{YhTt z;lz>+V?v0T<(7F0@4`B7qb>&uL}OBDqinXFO|5q(+G~R+#pc~-1ND~MO+lK2#mqX7 zfsS6xofdeF#~!2VbH+nR>&D5Nz-dDh?jfX6o9b51jb8>KF$sxF&9HLmFn>Iydtr($ zkvsF4{U0LiO*6wYHD5g0w-9IYo`Q9ss7iSl{NnGg?xT#PHfB-_cKtvrUjlPo7rMH7 zVr-?ihPj7Yg(?UVE`s)ogJ??D4!b^QOI0gMcCfbI&~gw;Egf*bswo?aGxg5riknQ| zm|HKJ)Vi99uuOBhyA1BXH@kJe8n~%OEXoFky=^{9!`$~LB2j4y-Kq)vf+id{O$Q#B zvLxuf1>pSjBvapJSBWKgl%{RQpp~seu6!~J9r;%HG=NU@L!bz>|5@QK4%T;Z0kx4? z`LYtX#p3ZPC3&l8yEECSo5K6BmAS{&Sh1GgfU4`i zGwdcBvd1Tf*P9jBMC9CP^ITUNu;%!j152~9332V8+wba1F4ui7G+s#aMVvQfe~^oB zx_^)%;vb#bT(-BzKMY9%x1TvMD;`*S>HYw5dtUUr9e+sPdvoQFPpLi*U?pT*cy$VLKvbTYu3pa^8s37Eq3y7R&(#k4cEe6-viU7t zoE1%o)IZ_pF5?gLsN_fz* zy5%*w%^+dZE>yjk;VBoyabVvQr9n;INr5hOKc9Wsml7hQ?#;Aw9Ge8>k}u9^&syD$lF~OBOCl<5SE6 z&!;UrH<0RgS|-GUF&%q>F&q1?MkgA5(WO{T_@m#zrL{%x^W5Peq~1_xynhi3-P^1^wSIkT$Ajgw7r@{QCL-+}$7G*)zQk_mrQ>s%5KKzM5v`(lerEbU z{@H>GvQ4HL>p0%5{P`}vQi?I%C-n5ozF6|b-2R^2Xf=9^>^Ka|;1T}`Hd^yPpg1y_ z0xA}@5DU%;0?os~&x3P`1!$2IpIVY(ls1WR;qy2SULKO%b*<&5TO+yK_aj0^(cM?+$THe*}Wgn-3W~ZiG2JW`Vx${ z6kn#K_F3jS_yASv?R~V=tX=${iWAFJ%MoJ!Tdd7% zw|pG$VW#kwur}(BXNmUv*(eV@mDYj#CM^+q4x%eh`^mXX4X^J_=K%!Xo#q~gDzoXa z>oKyW3R-?z!met*UBHP=|Ax#*T206zb&*#B7mx1Zq0t(n=(ds;i7Q$x>yJ;G8 zKC`VW4H}@y5ir&GSbbBq_EGbV%Y4V1)`L}1Kw)>G-ZA4lBnLfLf5Ldv=qQQGM`-4< zbmE(YQGYXr@P#r6e4()d;-qJ*ocVV~LTZc!MSNu+dwM4zs0;tAZglhHA6Ow1lFGXnN2=vPQB^H%9i^N6 zrj5jVeU`n{Ru*pSP}71q5kLWs#A?X+d@so6*Y67#rWyal3jhn z(ajUee||g`0e9o?BU_Tfd zU5BcyDYgy(o5-K_0Kftcc59tO0^h^$^T&J|iKCXC!UYUwJO(MHGGDB^Bddof|0~EI zLpjd1DLXYfT$}|h|EVvLK2$c>csEDx3be_({HPVku4MQ&;wrWTzdqut53}u*Hg#iw9{)>^%8qZQ787Ul?i?R?UhirL zUq9(V{iq>ATyIbd%s&bXa&=)eit#8$r2Ws)Sp`J(bx|Bd3F#4}dq|}l$)SdZVE_dI z=?>}c?(Ps6YUle3o}Q8novr^+&&b|2 z+xOf{kPE$9>7a{{5OV_!Tc>Z_J z1CO0rpFv0F+0zGlA@qJH9v<3Vu&?hPF#cpO0`F+R1>n9NL4jc&3VAVdjheymP!vI~ z9)T#neG@1}T)x$uJ>Yv7-dTF_Q8c$|uPhOYDgdM5qTqg~{&R=l2SCx!li~apk}LA; znZimaqt?_hp%GLQa@+dLLQp5R9FcPHI|kEhz(5>m!#v z2|4}_PZ8sc(x=N_mnd2cTO9d0J}r;M5d*iFt}m@e*MBV*igm+O^Qu5rp%!Dj>D|Z0api zO3Hs>iaAPc%w7(!r{r(%=Za*O*iAb69!BrV_G^&dmTE@} zD~xzOmk%#vs|C=opZq!nCf{~fXkkAa<3$)7(oLLMIdEY53BJ)RbA+QHv8xao!R#L|iCkS#W%xu;`bx_vewFL- zlTktzxYWs@PM=x#qw^PCWgB!Z2j0r;R8P!erEUcQGa+`}uP&@~TuQFG&7c1aFqelp zID4f?Vq8HXBd^(Y8*x%OZqRs*zWl_!XZ9$Ss(RD*$ZE$hcZ{0Y*ljXD>mO*H&LaiN zjWs73eT7#hlU1&Q`57i8EzT+Orawqwp)Ve$3|lJ_KXM<}5%(l{YEdk3O$K8T!o z7Bmvk6!wEwTt?0^13i ziPg#w120MaB)R`iP*SI}C|q(15wAPdibAmXRHRu*FgU{P)HG}3dj()6S?lx`u6FMU zdg1W1>nf>jcj_G%*J%}ibXS@X17u9!6ojyEG>!_7skv&u{D?jd+Z)CeB@M_xvAE&2 ztAxtx1%pM3Kvqi`Rnq65HFocPdA1K}b7C?1;;_ifsd8dx16C;_QwT_vK5x5;sAmgA zent=y{xKRR`~^bocd8* zlmye&(sEb+KLf~!PK|-@qb!Mj<03+x0^vT*43qa-$R->OI zeMe5Jb({9x^BxW@*2|t~l3JGIz(1a_BhQ~qoQwmcR;ujB5gNz&3Pr<#FU>v-G$C-E z4#~-48I@`#)>*oo3kEi5q^{~syl`c02to;ZVpS9IdO&uSS{V`Kj zFUUoo+?{%~4}sxKrU@&GeH`!Cc$NYtMNtm%)Sy>}_e=A6r+`Pi2lbpDW*CN0!HMu z)n;swL^VQwK~w8MmA;B?V@*_nRR7gP5eZx!QLL)toD&&hu)xjszO24)Aj3mLP%LK2 z9Kk)!MCC;_JH#*$@VjfVDl%QJSW*nMB5jI6j_J+#2NQ5tNmX-7o%`A9ip5_#j9vA9 z2N#X&1AFE6u+t9D3y}`^M;)G%20O*cV=p1O_^rE&Lz5i_|CYkakk%6*gm5kVF3pYc zVRoM~Ta&Q)6J_NyP%(|up5jy-Yt3w=g+*--JKrF?QfCIbnfT1d!aKL!Y_>Mnn`fJy zZoX4H{j-Upj@05sEP$Q2R-Lh8rSA*ooV{dWwb+@11^KI=@-lp~OBo&FZ>9Hi21R@r zp1EBGMOKuA3P8U}HENd$zKRa{f+=*R#L75uW{~wq&y}VasTQ8{sa-wWNfga!`%zwV zZb2Ak$CL^*mgwC`aE%}#ji2#;rkWm`vRZ%SRX@}=OafLe~0n0u@nR8t|VE7(~(Rowol)TDas1QRs5D6_{ZBe|ggR!9cz92MLSaF0g;~tY;>nJ-ay#(! zO2arDqbyd}Voc_e)`S)wVicg98dc9LAygeYKg0g14fy6CnKABR!0PrTEhSH!g)|{#_;u1sr#*!bJuVm@qoQW# z-F&6kkS$oM^xqdiGUbBs_RbhjB%}+bhqrf(ipzdVYowOB2f8>tgdT?i1lC+AW4%JQ zRCf949gel1>F5(!Dd@;ISm~(L5?qyXG0Kk@8%jl@T3N@}_Ea$oxXV*?1%n#ebcTOc zjYZ(Iru~WKN=-Qs;#}-HGzqcZna(HwFIM}9Tc=x2E>eh^%KzUP6`Qcdho}f#+1(9) zZdN!3-m$;z%ch=?=j5E|6av4X&P9Lu(sR0UhxmruoZ3AzEJ)CVzj{GFU78D}+tEC3 zd~9@GrqQ_4*Ny4h8*&YqOK|zbRA;v=z^ye>@QDp9>D$IB4uCcu8FBoLE=fv${}@b+ zny^?@-A|4H@~Govq?h-Eq+Yyd(^f;;Qy>&2qD`3TcqmPXTRNnZY`lLqMQY1=$@q4t#Zzd& zzDqB7zC3u0#83P@-)cIXWG~iLcpoo0;6^<=(C&UhJ!`XiRBT<>s(xKi!@XQfy!X1M z_l2jPWrWYuOq?%l%|<8aH2iG|))fuD&cC6a@NWbm*^dB7x&xK7!3~8I)*yo+&+B%$ z@VR~K_EKfB;nG^abl@hh-8?^n9yrm!M*W#j_Ahf1>)kkOO{p)|eh(~)#X4e2VNoVw zW=gTY8@-9KCJ6^(>0b+jw2~^81&?#1P9Ih1H238z?)dpV_K32}8H|V%6r)Kvse1yU z%n=L15>iPy$$x}vl8z^*iezv-<(uJSV=q7_D{U8aVt#dv z1o9Qub#oxr>nVU{Qq|L^t7GF6Q5KgV?m7HD9>;fek&nGKfF)5*hLZ9YZ$0bw)pdq#)X~l9>1kvk?=}Ig3}lF}!Na zjx$wt`d9$p8G;D;R`z^pgj*c7% zNA769aS+;JJ&@XeI?hj3IvK*8XRdTu5FBlKBW0e%lKbDIsC1=*b1*ivN!|B9;R$w4 z@KY4>e*5+NM|XPv3$|8x(`vcJ=hMJ{83`Pj(mB=?RlGMK<{d{EYn+6^!-#kgZtBIZ z--1l8-Oo5pIxLGqa_-6;wfJ(W*`@H-R&oX#re+g=t)SAdvXD5L%H=0xgEn&v+`{LO z<(<}h-NqGF7XCf(YDAA5v^xf1&K(&$I3I{4wQ0~Vfq>F^K`>FyQ`=l(Tu0}W8&S<3 zvHG%e3#h)Cu#%ON(abP;6=o^TArMUH_o{n`DWBIlw9eBC%)%Jd;Et{?aY~;Y{a2t1 zEF#K4o^tr^lA|~zcSK~QEMPHS=B@dDAj*fIM_O8FI%k{T&m#WV@mQr#W&rA1!aZV( zQbLFcbP>qOKp4z9yY4nI8C8EA(k%7b9ep3b1);Uu?q{@%8kNhU9IDC^rnt>0eJe9C z`0xePO%W}`K%&_QlwuAaea^o7R%xHeQ3^jEtv?%PBBE6CEOux2b zdlDnod7_Yh+sNaipS+i1kon2mY?2ykwdrsTXKEOKtp|i-uT3cbqC*xbOCOqZMT(fd z{rkhiD#J$VN>2lJJZnSEb!C^3X_+IfvG&Tn`I`SvhZ`D|t1*oA&Mt}R{!osf!ZWeW zjA}TA+H1D}f8TQ3IMR}0SLrFV$!5MQk%V~OlWe5nKwPq>#33UH#j@di5OGCF7#@g~ z6ZbOFxwg*Pv~5D#3eP`vQ0(~gn^dCVKqaRzDb4~B3f4`lohCEJ!Ftf%vxaPl$D_M~ zTzq7WVGDi|GT}KpayW_K=I5^`2!}y>4r&9zG@HPOpQ*MaPIwRcPLCq|4b3`mY2h!y z=c{ROx(;@!Z_|7h_EVk70`iPx`Kb&egk(rJIt@{GZpA-y#O8t<&zGC+?!(A?7!sQa zkGt;eNz(dKmT^k}d02Jy$Qj&9rKxzJjCix2=7 z4TB6Xg#Q`VQL*sRmqi?DH&0HxXslDH;k+C9jCo8NYS(nbmboK2hPWMAV{Z7DI68Dr+EC>{Ru z)0(6Ic}QjpL(4nG7bMv)w)6(0-ZW2)=ZnE@?T(+{Nk%Xf|7c(9z*m&f3$O_E3focQ zYI5hRe_z|L^Tpln^`JQvA6)4kVZ91`$MQ)^b0p0>(xOhEOTYA3VND8FM25YU;fNzD zr9oCpIznT7*1I;MqS=oCsi0BNJJNTo0Vs50hsC4ijMl+6wn$|Hu7fhCvpf7;qQFOS z>Q3pL{7F(KMwga&1Nrn5`6%?w#qI)V>^qA4Y#0|?y+G+Apbzba2duqVZZcwM;tTW$ zb;3(k^d~Kz&J6d1u6r!a<%BDa1DE#Un1sW}&@rKROrB127+ z1(;0T+YuP&qvmKIQwj7JUJg^JS3y#`JeoE4AVEGmWp&lRFh6R%<)YZA_ z4G=0}@8Z)~3})bRe*}8A*tVa%0oUf+sX4|q)u@@tgg5Jj*18N6>ckc6{_*z5*Js5Z zhjH~dsBCn{bIy3L=uPgW`#nyk=cR`TkaofEHPEu*$U(O^sDE5SFu12V3-~w9)_v!+ zba00>0^Z8;ort~us*eT_*z~*n9A1scs&UL)RG1nG{&wH~?cSSl{vD&St`w>RlxQqK z3_IW@HK_O*+JT2*b{HM}AZSyv@GeFYkn+hiVw|tjZ$?@RR^n3m-J>(g%+J4Jg*d7v znNmf6+G!sI~vt9sY=&qvQxH)SEkr$3GGVB{KWs0?x66dL+c6BL-DA z!fuQ%JoRvWD~@((V?6MQXij@^r9VLE$Iz^jeBb}ZtzexM%JAR^ZLZ z3>1F&JL}7lHrrefHFkoZsQZFsR5XEQ1Z+BVLZOI|4am^AugBR#%0%y3M$~4hvoYqm z0Kt`v!Y>J4Ukfb1Ur7AgMtnO^B8L&vdP1d58R)eKKw<6C_kaQK5%$6%*jTYgOdJqa zJ{c5iAVnpSP`Xdn5 zkT^o?y!R^fp_g!SAZnRJ5n<%#`e}!s!WMJ3FJ~TX*zj%5u@9G6_@z4Q>5oxSkG}2? zBKjxl;4aCEkrb&k1}40-&?0?Y(RYhqT@R{(RgU2Z@oJZ8q)gcTFZ8p&b4S?e#4}w3 zc#&?6yEoo+A;hVL-fY@kp**0lvA%SV%xfnkJbH2d3#bJhUqSR(wqx~Sg2j144JX7- zp?^|@lspbL_OK(TtDPfUVBg3<&IVMK530U{aa0Ia#I8HA%7##A#zUOo==W!WLr&5-l$m06D0{sXnM-uL0D?>cD^tOfRDAaG{3LBu6Yvd;P)=bS`7~N7X(n8Sgte%RT4>4A@RRK2wwRCO30&Vh5)i0?W)H^Ouv0n4 z4XjCAU9rb(NIp#rtV!ugj=*FoYne4mr*Z$t&3vr3ij0V`r1(z?Q@QTLSSc$)m`Lxj zQ8nUM_E7_)K_X5LackrD(O{pC7F;)GTaMo0u0eGl^0!*3RlxnZAF17J6W)^kWTLqLce*0sbs_e2zM3BBPHjqoYvQC_-zffiRTbT^v(_xj@LnyF6RjU6tP3p4wVAIpUOdtDr%7C%)>q zFq&hjvZ$!Rbvc~T7^a@|Z+9!*@UgwdXX}li9)-cz(XlBg$!e!OZ@|~p3fI*xXi=Ff zHe2UHpNeP}Q%YP!M22UA`ime)Js)2(G!;jw1Rd|F2!;N8x-3(m4wH18FHD{pGsq-oFExRelrPWX7q_~KT=!H)27HnU-8g``a(x~_RH8;KrdQ9E_Nh+WPK&4<*Knw78$VnA;kY7{e{{{GhmzxhwF^Z3Y^{?X~xkyku2P%|J|Cb(}!%x^PM zeqSIWk$c*OqHhx$CxA!VQnIj%6}Qucp#V70nB9(77LBw%`hdKcQ9+>Kb(xG5sb$K& zB*S#qZ#j-W;=du7$pUoB7k@F_*61x5esu=OOP|BB$R}2X?U;V##6~?kORmiM2t@t} zw&)g{JDAYm{Bl{_JxPqPg!8z9*zuqq(M~VXWq~W+4#OE$i#Vx!-ulDpdZ%=Y;oY|2 zx7$?`L!YxYHrLz={C{M3A72ksKHn_oJm41@`a31@Fe9OVaq{$d;$2R*E1y=K)oD+& z*ld?g5%bBR83$(IdMxKFZUFZ|FA5`}#<1$j!Re>z#Qnb8STp_5T6nXi6#v~i*rm}Q7E=QKRVTsroyBW23Cc=+9i8!>+TiGNHPCdL?oT4KR=wk_Z}ni3MD=r4 zmcY}~)sruFr+LPE{}F-=a;ty3G3WXn*%*5F0%F&^FR;T`mr~+V!zPJcG|{eIa^|R) z{_d<{m!TbchLz#KpZk(*`@1v4hdDz#TxqS-D723I80xb)Z$PghcWE%!DI+o7W_f>o zq-yr3qTLkfcFaJI7ESizF)%E+WKx^F1S{rxDk9<=~n)xQ8IYJnI<%(3<;70G7$TS-INZ=oNxVvI=PN>KUGV@ol`ib$ZBMxGu(iu z!HoU-t>Y2g(L!qegE6i>~33FWT@<$aG-Z85|YkEvn~ zFUjv+C0@>dC5jM$ndByBCVp|3l{f3X`dpO@O+q@2ojWXciRjv%HshYWV-Oh+h9fym zGnf1X*L|F8y1wvN-thPp;3oz)^+1HF@{FVm^+WHI;b# z5G|5iYRR)NQf6Er)NVADFH){~^M|XGwij{8BC++?w|?Hn;2&-V)R-xr&_aQ!alf}k zX}B$0H@IRZnV0^~oaCHLGCw{VdK?z5lzL^{O+>lAav~BTS;Xz>8Jca|{%A@T6SQMJ z@(0SBWGN$aFEwUu76Pg%p}L9Hea(P!Mu%H3F-$lrzTcwq!=xp1ldSjLlj3xT3s^oe zf3=-mawY4i_ zslkNIM+@dhf`GlED|2bq!F|3Kurq zi1$m%><1HpBJUVhPN^Mp`+YN$Q54uR^a>0f1@+yUaTv!4!5$9S#D<7>SI^lBMABWM zv&@K=z_>-RK`|w4&$DH0tqwrLc!YMu{Kf$3vd-d5!t_Sz^$FwjY+4h3!82?KMoD2m z3=NX^x^&RHns5RhS8<#VvX?fiC_{`~-s5=}b* zffc4w@@jqh$h7rdjuK3hoo$D=H#C^>)m6k7%o5RU&$k(Z1Z~i+5$&an=R7|gtt;3b zCc@X|tTVS`YBILq51PCB${28Z-!N7lOth_&Fw4IL$pb;h8dVg8Qv;F*rM|Mv`!LLuR*fb0GpSb2X%>hi>4X*Whcc<9 zL%}HUs3=pE1pi-zsA?6C=c`;gXHpgCqb1RAho#`d4CiUp5>-S)xH1s4_A*E>i#4JL zIgm0C74$9=Hrz-p0#5A2u%K{mRTy~reqKR2;V;~?t2$3F%+iXvLk=tD!O;|C6AUFw zC2NxkOZdRTO`m%AmZ}5Ob$Mc$gZDf{`G|D&oB2I!9mCnr*s>$iaoNdwLY^lKyWip6 zVjsGEgo_4|aj!l1c)eMY_~*^apB5w{c3f|Kfa>&$&x2N$j?)t~CUFlGxz}j>F!UKF zfG&IcR({)2<{wi5HxvP_G9Wi=3xw*)=FxHKf9P>kG7(DWE|AI6IY3A1w{w@opS1yo$JS`$6_|l)cpDUBO0~bxG@Y~mmf8;&}TZx6AsU$WrlYLvXR~g>ItZQ zYX0EKEjs*OkcSBYL_j7mjU>>&3bA|+{a9LU0V+z&OROlCZggWhp*n<9mbu;L_n9n) zJ+Cs&G-EI>#AVH1-xO-HiP`rpch0{|___#F`GH?NS=&|S&81*skE?mI)J#A8|D9K_ zXroUjhtC>Rens>8`zJhkqIJ99ow7+?1-r?Xf>AN}ZsI^KuF@$Si%vEM&h-x!`1D_>S!IFf(8Q+f)xiXXTOw2C;tVbhxJ;)~!A>?)~R0 zHBH1hWnuNCOn=oRdz2Y6F1r}OjA`-`Lq;Q(f{Yx&zo47u zOeDtrhJ7%DQ_MZdIoJDxJ8bg48Plj45gC9Yz-Yb`1FGryVe!d9-=H5+4K*c*h+MTcFoFeg46kux>V-YYBK6v@Oi zIt>Fa$|Bp8@&m@WDR1@A$yAor{s;QT2r0DbvhU`ZZ1d>ceohku5>d8o(EYbwrG^6MJLZ-)_lA`}WmE%uX# zNsFZ)@Giqbc8tRF%87gdj}7J=GKn@f?071nT<8Alv~8K7uN9Y|j#bm~%tqU#N5b1z zc~y_(v^~1Jo5>A*g~(6$Wr>WY6-ms(ESiQ}t8Ed|E~KM${pv}k5JoE1pXffW9wg6W zZ#bWqf6D~KML&+@DV|4l!u?;R@*xf${i}&<44K-Q7c%M8AbJ@-EA|3Vg&-O1Yvq4V z+91voWujqTdgGrc^oNaI!Cn3E_U)#F$Z6ko#Ov>QitEHnEUsLJBg-;&on$?QW4v=5 zFsqFIF0vqNKKtAMYLgs}Lw(_tTWp{Nub2zGBInAzsvwc5!}xQI zY|3fA1U1B^Zus2kV@1DTNKzd>vKr#!!%XZC9v7bXUg61>2~QS@P_S`V^A%1rmh*DI z@7GN{uW4z~3EF2iHoq>7n*1+8Yvkhq z0z6&&XRN{v4I_-Fei_6N?>k(zHeB+Cev55A;YK-8Ot0{ASvAt_0snt6 zF~5(4SpoOIKLBgU@z!N*YG|o0IjfU514q_={DN$x>2||8P3Imltjq$Ba zd=k)}yHjB4eqZ+I;{crk*s7Whx`xV%^`km8T;&&Imhdo&()Op{ly@kDR3^g*V^)zE zl=~-Rbm4$OW7$O*>td4JXz%2$`m}CAdXFUTGy?fdx|K~;B8r-7s(@N*EXt_2c(L~# z$EXz+308LE1q*j(IGc-`}5J7}{>MeyAN zn=iSHqEN1}Lq<$bKoM-$mWZjQrT+?wNLygauU-4{kipnyXk?}8>iP=^>Rn=VCJirM z!hQE!v~Bz5>OLeJruIdFn00ib`LcPHoy=Tn5EELHydL_PWRWh3;x0uA&j1Rmk3cu&sO&6kQ7OAw2L3q>SY zt(mky>AyD?`+g(~M17U1HiU8(?mL^)N9PR--S064VNFQS8Aqs#W$N5+g#MtkAXh8Q zoaYpfUZPdb&sptw&(3a?#-rBKej%CFCUb1>k8Xdr?6Jfo?kV;}f;zf+v-Lata~?R_ z&2$+1`v?%$V5Kw6^G1L36@LT%zP{XNr+;UIrAABa#wzY7Yq{lWq#u&1p3`3$c+P#{ zLgD{(5GW?165Kk;bkioKM<8AbFc|>^2i#}M;k8|+Gf%x6cKeLLe0;WG%|uhqgA!%&a>?LT zzRFUp0!T?CE8$WfcYV1YG5J!8XspbEctP+{aw1YqHJ@xf6|At7K!EHO*_wsy@#1)d z$$SVGs#=AQnq!;MC}BX&Zh^rmLRh|0SH=Lw$#ftDFVH@A<>F-~H&8$FW!!*~-qtiR#&h8pj~ zH2UW}AXX6|nv@e2t~S}|2)vY`Z%VRuTXsU8L0r9H2|w$0J!NHNMmcQPyE>!&&?EU= z_?}r#08vs<%`phrjzup@{AA}hEcu5~??1&Xh0=mtB6(&#CLDFG&hSH4(Pc-dz_e(T z$d36qdz&vij-1oA_Pg+98oe)}j+4hqOpzu{lP$EL4TjSgs6?y58boTf4d>`jN@^0=#Z{qSmeu z0?1dzpQ$TISyj34C;$lqnd2}LG}=Z`y>lDAeJnLwWI9J!O_vsw1*;8~QsLNDe`t+S z(H;Z4<0bbaEVEPHu;b0_$qe%V$-hfIl`w| z;zUCxGp*-g<vI&RLEu21`XG-t?e!yu7 zw_bG#0urPpflWwac68c+IQ(kf|N&KGp-CQ76q!uKDC3SWM z#Rq0oFS?k60nU+o1nk#MZVOl49nYYco7Bh!O&Cml3Cvf5HQf;<=x)nm7W)1B%vt)R zy35DjKi^9Q8V9gBJ8F^me*J93e@#a?++Jd78LIBgMb9L5^~VvYfMmTkLwmMX0v;|C z<*A3qCn84Y`BZ$8BGHHWc0$wt#walP##5LM{q=XtVUh%fSKH5$a^JbTB~J!C=QR3^ zlMGFitXd-SCGkxrz9>;Eil^_(vK~ZUN(c+&z&?aQk%8je!j7c;16mP(di5M^)f0ZHJ*W5AaD6M=TH6AOIDQ27hJ7SbJfs!pMs}pet`(munD}y7h@@eL+ zysbQP&V59faxoewFpMVrcqi`#sGNL92;9 zwtQF%9K}Z2p6xl|A(qX#--B&X%0fOKw5lK6^LhC@Nsw2BTs{rM3W@^a$=jOIf7NyZOxbWn-9NNAd)a(qN_D~!(5bG7t&V|?-45l%(r$7k2-DLW zp9X?)6lG!yk~N@(0gABb*X%)+`?Pu;UW@j{Ki^*`{TBJ|7}gJk=4<(thjDRo8u`g> zCX`>>Wc&OpqXU%>0**dCg>DqjdV5&88cMFfYcP!Mt7-A1B8*j%zF6aPE+7rNs#OTz zit=$otYp~&Pe_=}peG7r1`jBoIdp5?M^-ok+nWSIMs#eZNOO zhk)gv3~7h(0etlGXAz`x9I9#{LPBoRz^~1jitrSpQN zkDsrrLioQYPjgl&C~g1QWWD(r3P(Oz%D-oG)|5Z2MPmtcRo4Y-WWzQfXjC{E8Yqx9 z`}))7Vs8eyAU}`U-5O=;G$%ISN5{|A097Xrmuf zwPgZmz7h*H;cw|a-X(K!qan4xFs1(5X3X4J92>=c$#s6?{)mnmolSsRn|2*Dxb$*5Qq2(}bhX!abSNGZ zT<_3FC!|kA7djte&?x50`2kP}3A5EC4I8;g*}jHYzm|4Tm4~)2sULJEOJ>TD(+>b5 zR2*@i+Y!U@gMiK{WBKy)Y3zNvw}(Xx@^J_U#UmixX-}Mzd0x0eJ)_mn?yv}21fCld z-!J|}X|2D?ZwWD!1fH*qejt{j1>|j2wkFH=!pNr-gxqR4oLWHI^{GbrYH0~@=+oi~ zyh-#*#chr&TZ@g@H8C9lna{+8U07=tAHd%+cWeWmm#9+vH4qn91oKR$R=% z^;C`;ss)|kY-E~L2oymk`uA6`-4emeZ5LDERl?60NY*9AIiUNj-H%PC+;Y4EU2av-5 zlD$olzCjm<;WDf_Rp3bb7Hpm>li8YU4aRTu*81BblNRmCP!e`sIIRBdB9HUXMJR~@ z^h9c|@A&a_Vb!)_j8Y27Kg)^Tp2$B*(<1m{3dW*yj6=lR_FME@f7z#O0{ZGbIK7`5 zKfByLYdgLr&D34f*F1sYmSlOgfC}z9(q%!s8P@Y)*dZlKjlNtu=sy8yY}6Vi$J;0k zD?N|uRJX<>P(s8J_K4J-iIN-T{OadHDvhzi>LZ9=s4fWz_nrhH)8WzNZ+VJkAhHO{aYBj2Nw4^+pal~PYLXBI)Y`2MDx&j_#Q!A3T8@%(y5FO3ikWaIPohGNfwUM zIbo)(mvHR&V9XUJTEa z8jXv3XqpbEBqXeRlkhe>L7@4p3U9R6@(#@;$f7{&B6izQ3MkWhw^4FS2ygAEEaw zEp~u`kH@I@dBJ8;xhpEDLh1yU+I2CtL$S?uAcpKazrycqa{5GVAogBjMYSDB=s@yU zwn~bYQ-yn_BRM{lH@5WS7?H*-DlA%%N&jU(EHeZY>_sxBVw&!=wumyfz~fRBRwU3F!X&PF z#_=Xo1~U@UXwBg?A0&wiDo2IjYPC~F>+0;7#mPYtM9Y#8+1wFYq(FSZ%$RrWYX{gRe^_@@+4`MTWqngXyMJj1N!in{JQHw>b$CQ(1gE68JY6K z*b4lvJTRUr=F#U(98)hszFX#R)??M-Mo!LiFy8AD7B=p4jMDg=N|mlP9SM=5{!FFB zDpsIXIGR`)yM4}$1!C>DPGZ^D|9QqfegFG)R2PqSdXinZn9WlFMyNGA$jU6GL@hsV zys#ho=4}-^X0TBl)m)TsO%?oQM@Tl(U1NuG*IH#rI zkG;l=6|qP-HijrDINA^EA%FihpdG7R-5;j+uAw~#rbl%AY;R~;t=|zETI0qGG4ynB zHif;CyhE*xga2qI;8Q&}gozqZ5EqT$iMnG|O~v=0-)+C<__z=ZmF^t{F=N76&8Xyw zpQ${S?w{O$UhiAly_z0;G zMJl)AYqM~r2~^{2uX1bc9cbpBxSshj5G#7*eV+1PDBbaEm%PW<%R8f)Gcl{03U!*_ ze0OStvr->}Yu}-tS(hRlhHYr3B0`|>z$EfG>2W<0g;|p6Ed7%GFPbWl7LYz(vcXtY0-gr142DkY*OarY^`dHY6v%#AXI4}6RFH}Xb06rTpH%Wnaz|*x$2KvoJXLu4r=&ivs0$(<`O(0TueUK z*;+d{{GQIGFSMQdScu?XS!QU_>V@NYqG01>d2OYnOXE`PIEA_T-z~42^^!U0l)%FG z!jC2o06RRR8toYEYI$wzznHn#UgOn|8SIF#Fa8tJB6=kcq%#bPA@~&`w+nz#{2nu@ zBoo1<$KBc@+D^dp{6~($T&L$9r;i)~3IXQjSEI#7>WA5qf4G39m2)z_Ff}X)ZoRKR zG@GuY)al;IoNH=VT(VY$*e-tGYCoZt#aBM6`UBdyj9QJ&*@Z-1OZ~|UG9HSf_BVIW$zqf8i=4-jv7-|#kp>t2ph;B!BgH~z zm=$&19{qh4e7AeX?G@hs;WCREA6K9qFQY~Pyd1X1a)c&cu)Y`X@O7^wlucH=^x=EF z%T4L!k~~x}(4LMTy|ZOcL=e&cdJWw-4jn zY;qW;o1=#@-8GJ*k8Xx(rn@%X-7y^9-Aw&-9o^mC)6aL$f8gcZ_x=6Eb-k~%2wTt1 zJxrg!-MPa$dC=m?>WQF$gYPsG?l2`Ft3-XjNzS@Uci`X$xpc8vsMTWrmE3=$dH8!s zvFgNK#@28Ba~(Cs-l;T2Gr4O<8nr<3tS$JRoKdD&q8XtDNn(>{_)Hyv9~b9z<@4n# zmIH|-qsFj|LSK^?ylET#nexT7SxFD7n{bFflqadl?3ef?m(c1*sC3%68M#BClwZ$2 z$7Ln3Ugf)bNh&{oS3k_Fm$!X_TM)K32L%t}mTS_ssYS=f!SEA)Fyt<*#7p)5B2B2p zsu!F~v|xWUFL41Ecgg+uSZzH+dnaGsw3CY9xaR5aec88{uT&i(_Rw^w=f0nJdg$O9 ze_eTxcA(GUbfq-s%=Q%sE3$4EDyT0_EQ;l5X0QmQiYb5nFGP~SYn{5z5Vw$Fkc(Pb zx0H?zVbQTILjq&|?PPQj7NBpItqR!VMc1=l`q_1KR^q64rpUl)It5qgQh$ha- zYj<<)C%=jX_(SH56|F^ZbCO=GV_SUfa+cyR7%A>?^7YQrpW4KX7JbXfxm*I>fA>$( zb;8M>!EOS{xY4HGD~^U-&pSN>-sVi=XtNYkN9VIc40>xVy|x(etna@jbRW10X~7Wn zyj~U=e6S}2VQ{n>Z{*g*eYVnAEkUNf9OHF>%jJiBNClIl_G{J^PZ!OgR=o=8-D8Ue z4Ktf7T;#fN4KedC)P%H!IVusJjhUCU6)Q!Y#G_E#4$aVI$*iGezNd#|qe!<292>^Y z#5ZI85Kc^wIF)WA;5z@|D?}F0@^vKs9)`Llz0eM@S?&hPi_M!|E3wS*7Iv4*pVoN) z>zw&MJhSEeKjN1>pYDPy zLhJRswW(KVA%XRpQR6!%}YaNY0P$>6=I0FyFu#T8~p$ z*3qyPuPm>rSL7F%;nyJEoFJ!)o)>$-dGA@ShO%zcG2rBjJJnvq!`Qhdu2#~)Eqx6) z5XXIRfp%uD6%J2Fu*Rihsrc6%a)$y)g`(`A6Wb^a=q5iaY$u&@DYw&nK z{*Aqp_Z3lA8F_?vdz0Fbn+0R0PlBMYQts#A@f?&L+2TQo(%C$uZr4&# zIjhl3s$|Fo6Nvk7N@Bp*M7lgI`|UrU^_!_?GZe4 zd5sFsbT~D;vKqX|rW>cChhs*AO1i16#4#1Rg8hDha2B9iq;Wcf?JwcQ6C$uVf@ zd&v6~l8-$PUE0!=Sphm772Ky4X-+(Mh9lZ8tPwXq_%0+C^+LcU0bM`JdGTW!QYEI12`S$tWzLpj z+Z+z3e8Ep88axABDZB+-J^Ks6NHs(U0q@Z8NIVzJ3)=pKVlfU)A8{psCcya!XqG>y z(Aq`@r<$^pT-V%$=#g@WQQw=bo=3W*S3D^(oc(92elIsq5x8yaBt+0)(f5AK4={Gu z`E&U%#WN`X$0W*CGNpM|mAf&Cu!F2wWn?FRu?>gCstmFlTQKU+RK>;q7L~T+O&|jc zM3kw#?%V-F+*?FEcRtkYewfUkZTiZ!?pj~>dZqoIMcx(Q#CS9mr8ILVRm*D%N_J>U zh>;S|mtoXJbNaW!`qDH(@0a<`{vgEhpSFI9-E;4%6))_gR6%*fpUZMW98T4b)q{Iu zyp^ERMWVMuS7mS{$*kXT>~YTeoh{_Z2iG)JW9&T#AG3aiL4g5)aWR(?wjwtpoAaJG zT>^`KyFgTJNyjwoGQnll>lPX|`#3-5?OZwr!(PuG+w&L46ucrjCK(coW(?>z#zJv4 zpeT7pXv&22P5UgP{hhmeKuwa2-^Y0Hl;C-ogMI;&c=yYjuM`NunsMveZNd<-AlyUW<3{$h^gEo& z_kQkuIVmv`+$1W$d2UxcP6fZ;9L<}fD#|otCr0ADxWfZPw5}H+2qUjQWAF2=9ozcw zt7@x3h0q_&xL(S6DP#J{oJ!s9h*CB>hwRO$ucfnlfpNxP1F}{iwW;NV?ub?xuo|y3 zPLmNx*Yde?{R8G;R8CZ?Zhaop)6C$j8h+i?DvY#%Brx{@lFs#EaAD!m4r)-T$mJp+2*d33d|*VZDL8@qzU&C9g-kzm0Znsj+w)EW_5{JMpt7^EAAif$yN~u@7h2Dg*pA4KUAz= zgY!|KMX~xU!`^Qx)DpyWGoUi?wdpqb>5{jhI5;#jKiDlaTV2^=SxKb~foS0-dE#Z2 z5DlEAzS2vt! zBHbS#H(A9Z{+gGeoZ8vh;dqD{<#*D50y-z+BxdcM$fv0Sp=A-0vd@ zgI9Gy@L80n=&*ak`pWkyflvnHLGPIu$jC2zZHBi80rM);VW~+mUtpo?6(+(zWmR+T zeI-iq_e9+43vY*M^kZt4T15w|+4*(iA(lweiuO#0#z4Cp0iC8iO$l+q0OBx(j|YE4 zsO~?4^B74;3=TUDX4*=^zZdr1A($uJB*;JeO(#op(rEPhd@x_`IOL0W#=8$dSy2T?$Vn=jC?$ZV@trNn+H=j4$7=cVO#<)q=tvFH1A=$w`G97wPpXx=s0RVEDzGS zK)di`$1LgfComhuNMnTLRTrC5`UUbjFEhSqHlmSo9Re_^AwhsOOis>j+>eVrVXL^l z0TaAkx+GHFO(az;7JFa7ql1RF!{JAw0&6v|n!PF|dp zws_vf$~Z0Xd)UK7l((Y=XaXH+ss%{hDI&&w0S+6ajkjvjIQEOgMk%FA2m=QlTSLl= zXZR2Lh-RK+(=0DMWtk55gIM3*TGd~;3kAm&t3*WEWUJ(tf9@(il`rzgPq(IWWA7vW z0ZJKu)n+TSjL27ThJW}ul-Q4PDEgWzuM0hu5-Z@8n^ zHE75Ro_p1bb7zfvg0XB9y#D}6C3NI>TMSTwFFAceT6TK;)jHY*@k9r(_#tU=b)}G6 zihz4WE%)1TSxhTi>!ceUmzGu8#TSK+TfuqDiZ+;V_Lz(Mv4&45e|vdiu;rxusQG)@ zvX5KO;cvANkf)n9RrRss!Ix|ynhy<20O`wkcK z#E&7QBKe-2MQSve^6(cx0a=Iji|8VvUg<`t;6C8}%eU75%@>F9UAd16Q51H{(MLU` znDB&QsYw+f(d_p-_In!FS>$?m5ceM1=}&q}aK#|=!E?^-(n#3k6`r)jsa388j#I=} zlqud%`)-c+PdPJB01%9lwar|6Aj>h{aU$>#8B@ty7PtyLI(Kt2>^8lgDIL8x)vZnP zp(THRI|aoSQ%?p;tlS+~!W$+k52BX{fsMXKu?rCrO<--nZRVNrg4ZPKkf6q62 zO$F`yj7g)G#Ma&tSi{nIDiaRE^bS;;_8sa#=>FXlcBv|ZY#_ToETN3ER;Bgj1e8y& z+F1QQ62I`t2;EF!8vpIcjE2`#NbiT-#cD4wel?l;rS>T5?X<;B({wUMHCu#w+<+PO zctM%r=v`7Sd>y*QzhL6|U`gfUrn5D|KLp2Yl@Wqv3;`)Ic^$vUypxK6jug@H@Vg8G zp0Wi2{kdftUp0{YTLo@L60@On)MBeU0s5k~+7Ix)KUZvCHC60RvTfR|iJ|rbv!gMQ z^|1%@V_vdYH35yQ1L ze}x+hbc2XJz+P{WPCgy*hOL*Dq(Q*R;X;(q;?BZQd?`J3U%^BJezG-jo|d5P)H zNm$4D0Mug_R)e`S7cFxl#lH>l4=InsV7RGaxjr;8kf<+?P6H(A?&E5ZmSjnEXg4=R6(Ak zMrtVRtkB~4d?l)Bw=W34j_a^wy|Ina7s!F{Q^zh{+ur#sKFhCYznI48#>+}^$~+*j z4%SWzGja<)G2EN)*T`CY%5TDa|=7)=jXgUcJgtw~&eE)|PkDQ>($GN7 zZR6nOaZavfZ;MBoYOBQ|?&<+aNdUuFlE_)jW1q{K-M%Gui%Xk0mC+WFne)4!e zy=V&?jeEK4;(edG{A^u6FWK%k@wK{Gkq+rZP*ob1hLA5yLc3_qCSlXGomh8ITB}-+ zZXdd(m6`N%dr}0SBc-GpLFeV&lQY`N^f-y}d%J=LWf+poH^V-9sFAoK%l9n26SY=3 zwlSbcIHX&;x6^iR;3`!XmbLb(MPa zg-s^0B*S6ar5$8(76XrVH#hBB){MO=|M|XUPix-(9uI&}$f}|w=oVI1RpIG&pPszU zYKN^=}`Gk_$mI=-LTIfgK6fvP%#1qkH|eXBg&bT-Lt)u!M-nz;_m%> zUXzqas+1e@SUn46#Nxk>{Ge1`pYB7!y0$EzXSa)uNz8$k8v?q;)9MxyrvvqEU8GaG zhL>lBDS-qO9NK;*^1Ebj1*eOqu}o(C3|%_gd8b(Zr|#2bd6u>-DDO_PQTnYl=NYrP zN4yl--#E`Z>PeHW@rx7p$6&sc%fBRq^lQ2XE70sNx53M&l&m+#2ZAmG9hPR%Vqw~BN_V_iRSfR@}0ELD%fN`%+ua{N-_*QQldtkHfz%fLH?k z#7~+VkzT70!k^TiHEz95yqk7r=n@Da7bge}(PD>Uyu0ZR#q~bmt4Y&z$|RrDH2YDc zf&u>+bdv{u$EByoN3OxQgCakl+RU(9B@Ql5*;Qk~CcYHd`;uMb+2ZQ-et2i&Cd)!L z!SRjCL)|5ZZZv9|`Rzx>Qu_ zGMbW4k%Y!55Ql#`k0_a?X-zw6FzNXgC{5W_z6;*`_Ix_jS6I9TVj$G+7FWrlnQSvy zvmX9yWB0bIS-NVw?SJxV|9Z1%E6m1fzafs#lFS;0&m1j|!K-2OxR1HrrC`N8*~fj| z7^IF@H)#9Pq#<-a5e59$ygg3RsFV;Zx9F2KC}tt;sK00G#A3kQ*i+>bMgG;OMI5pk zwonEMNMSgnIND|ftYYy>StX}+6VoU9t4vbyNKH{oLSaNf{MLs_SX;Z^!hyX!B%|R+ zw!p*15ImbyFAe*90sF2P&6;|hvPLQOtyn=dKv9n1yW%(dH0V%53!dG|K3S^D?E~jn zY@`%q(0ZXvwcJn=ck%R4Fl)wxEkCXp&CWN(fP*l}Go5RBhz!XSBHos=i%W$kB+l5_ zKA7s6$J`rIZ&x&Ut2}!>^ld+DN5&FYJjCeO^==m-U}x6x5I&usGGF8plgDveu|d4O zda>9yGmN&AqsUn3$R4~23lOHxap(r->~OXLV>XMq&F~QC3%CA>&$kptCpAp2qzcCV zh}%!m(vz$7S&v(ECKR1%Y9J1M&7}&}Uks{XebvKYiDs;9|Jt0oy1FlE*gvKUmE;+x zujOf!i7-Z4otj5@i3|&lY^?x5ul-bzFiF3OF$dh^7>|FMF9eg&Da-w!$T(WM09X=; zWV#w>#&SW*2t~p}>g6ncOi2yfEy&xjk zFT%fjJl%}^SYKtxDlS25FI)+C!56&Bwh$B{nO~4~PI&iFLt5?nRa8%7oTY%YB`uY` zd5AKScp0SsJf*!=vg;$A>9tdIMWV_%gwDN0q5iU=Iep{UUulSLzY}7c$nWi0=y^AF ze$js#ndtL;V03ZF6cx?H!XqK%zQw}s`DoU(b)&wmtS-RpBG3rdS`51=LDGvWz~dUK z@V+jY&6vyX`18eA!s+)Ws&jI83`Q{I=A^Ydi(kldm#t=kq8I4cLWO}NsbH8{`0(ofc@O60Y&Rh}oo*UQI4TiIkD!@4H@#UF6JA1H*e-?-86R8m0^pBalF zcG*^k8*k+1TpWjI@~B-sOE36}9ET`=b~x2AC_k{5Pw`@}@AW*0l;xA#VevDZ2jeLW ziWJP$6uqbQw}WkPzRSjY9TG23DEA9Jw%@6x>c`cDluT0n3tIb)s+-d})%=6pcKx7B zhZmaLMN_Z+j_I>@dE1RC`!@*}jzwTl$)H>ym(=odQ~lW2DKwO{Vx5~RN2@@vqgJ3m zsOMZZn^-%jV4FdszWV4NyGO;qB^TfF^60T!%02Eo4^cfiA;ne*bqjDYb+#eLIl4ir zw{x)>w&HMAf=gCIR9lYKmiU*X^QmRX_qeCgiMhi6gW{Z;m|cGGoFWb3JI8rrtn`B0 zJf*gv8GYZ}II@Ttf*#WD z!vzX+y7goa#p!nc5mtNNK^buQw|h;=<4rTY^g`-7?@mn$AOynAn$L&++(WB2f)I=) zAIpUHq@iV>yRi2X9};})eMNm!)alzt`L}cdr1^mO&!YT}KKd9N<7mD!02rWE9$`%K zACzW>%5UkF2ZJ=xzYL5?#GNA;gmLSj3?=4cK`6{9!M>n=9icv<{CmPDhvE&@6{f;e zypY;%QHwEb{!4Rl$NI+X z#L~&r!OMkjQ*sx!z2Ll-bP*_d3W&LJ+-X!VmwA`9xu>x&EtgUD!vl0f>$c>qS%OXq z_-~v?fmgTPskg(p#kv8ID6OcG{hf`o?_cDKL%2yl7#)i>ZOl}Kb8ycN1neHsDbopq z0Q;D%X5$&>-O{cQD*}oib>4%_F)@dRopS{tV*(-=eor5B^ZLrjb`r5Fl<~8QCtCp( z94VIS!^RZiOrFImb{~D`)aa{%T;;11P-jzN<{|_(rxXvb%!Z%P4Ctv&Tp^|xSQttl zIDw0Y{t`CID}``m$w*~*`S)~D_1j4B@uK+|a)f))lDw0uvw5K=7;kjx<`;WMke}H7 zu<#B1K#f7X`{+XAnf!SD+_G&OJ4gR#xY zeIwU^DW!HfXddPng2ehQ#gap)gJmcqaUxi!70P6G9R2ii<1n9ZQ~v0<71Z?FGnfSI!UxbubE)pAfcK`Uj=~q#cfrY64s$%@S(R?g07l|B zdj*8`*^3Ha;X(3JBtkc1@(@+TdIt)_Bqgg6i*P?_)3jjth(^xcp$v4i*0$Vvq}nP< ze@W6u8)f~AZE5p&t%eWml8oXEwSswlEi9a7ZXdVG5u#Xf^ZbtbUTO;YO%P61tV`i5 zfHJRi#`!&iBNSa{)oz3TNT_cPMR_}r*e0=s>?jtAER)p|T0i%^0-HSJ=`NBwWjys< zbiJGF6Qa$q4Jemau1i}v)hokWz)K_Qfcw*DNN|v=)#U;n|d!+^j0KCukCQ={GOvI@D!Q2 zh-D*q%J%d}7pw?^yj!AjU-?OW;JmygEAA^=qN<>fCGoM`GV#x|l=(3MT6{g%?Dubz zwUT)BImGbZOv+r`tGJh$NKhS9kJDdupF-hglve@%*M`;Z$hbu+#9F5M?o~I4yC1)% zFZ9GfElS1T%4y5yI4iaJq$ACEQer!|EXlc%Lnd`Ukx0={Z?wmPW1zt%HD~zs=U8c4 z1Fi3RTUP&Fk=)p)B|dEdLFT}CClHkO8`$Z`RK-Go*Ntafsnv08x59nE<6%49_UNj> zURQ7R=Z)d#6iX`Q8wC<>iRXtnkN#QKMv;Fihry~ovO_4r4CBw!3V$Pl%**-;=!Lt- zZQaRV{E%AA56ve&_mf#Rgx;&A~1Jj(A=~@VDq{uP55zP z+=Zpe@j;VWHSxu=gW_I&oIQ&(v&GLFTmq8MjFMC)60#ds^_Q=urfrU?NT%MpIcGpV zNKt#am~coW2V8yG83A^T(qTuOZK5};cJW)Gy440J@Z#bXNROr*ANs)69OenV?VM+D z>!I$cJ%ZTa#~YJ{5hox+En8fJ)~yZQY`NK-+G5#52-|!$_iFgdSoC;%STsJh(&9k% zNlC~VzErGuY742qSs%E-xxh%uWw%n@$+`j#B5TWlA*qP5xJ}#GIGsip#)l#(>U;ey zbY_+#{8KMULL3V)!5p|y9a<%?$h%^l@vgstAO(lVi#EoAft~Gn&n(Yp@buknaj6VO z+Z6#;@&%WlmI@>2PSroW8wibbGp0O%Nn3Ne=ls_`|3{HhL@YL=JGNF|V$PvuZ@8nvdf zcIPk0_?8s+DWk|?PSN~u?}*be;FvlPBL-H!CyW4XTsvPhKnfmPUN!xU6i-2s4z-}s zPvS@bBZ;B+biSfi)NYWt_Law9tRI^WZ2}#qyNzjE*G((#BRy`O-u7dix)roK)dXCu zd(q`1$dP^-qL~#)Oe8!)Pw3KfugN%h*csWXM96RPJ2n-5(3}U-5AyIjMG{3Z0;sLV ze`#fERkz2d@{cY;WZK7S8`o36g5W~FbZJlnpEzyXHm8IIMD41CVvFt~w=z72Q7I_G z5AN5eYj(g)-g1a0tGm6X8<$Cs*tQc|&Y$-it)9xSEy_0=rXnY9@ z8GT+}Am>GNH+7_@MC%=Ngy#Dt8Lz#uH?S2X%>I=Z0i9~$7PvV3Wxv!X%t*e}CbQDk zr-3hJVKyVW*R8-Bo3vYh{tE;iw<)<~Z#d+=W*{g*2U)j#faJFot*Ap3yhItkHji$A!q@I8nx=SmPtJ2dgX z_^>?{Sj0zELjLgio;ZJhT$C^ffDhG7QJukP|4T9e?LNYAI*ZgFf`eDkC2mW~)=DA{cgtDgea9yYuG{y_67vot9w2v8AuJ$p)ditog~Q7QRs zJjC2UBA4U7yej<^)J~za(T2j?bbUXxeV8?;#YH@+ajo&6kxxdQ%FUeHw_cLw3iY#M zP{QbAk4^RR6(8@qKaRV?>OAHj@vLFVIq?4B2l^je7YJG&sM2!^7;iD}!7wl1@lhOX zMDzV0@fx*eN__y_JgGvbDYo{z8>C2@+1EoAoB3nda;zzw*{@!CswI2WtCjTFGEN|8 zPWLq}gL4pk3b#2U;_6LCh8OX^QIj)zVdAH+zgLa@)&@j^^%tkAn#W~Q+X(x1ZAKTE4fM`8yMTYtC$ypm0i^|}r(v`Lw@5#eMr9t#&uVT)VsbcsXZ zw~4zwoML5MkvfeqRr#l`jEf{By>jP$GGjg~aE6XuXD2&{o9N1I`|Wgm+sBu;=}(n0 zsVHb6Q*`C54~PhJ9u@PVfRqO~j}!|aWfdZSoi+N-;3GheVL6G}@vf-00wPj|LPInD zyzhs}@>U2AR8?}z9GXXytys59l->NFWAD|I- zS6p#BD!Vx3&x&(H^PubggcU_JLBD!;Ap~6!)lGU)H9Seve@Yot!E#-xxhK7+ci8DI2JIgY4q>7w ztmegWt=>CZVGQ(D-zaIwFoLozBvAPjpSWh7b{Fc{OPao=hVx~eE(#~`)I$b_&!2}E zzUR~^S^t4S4>joIQdJs8xE8}F>%YW9An54m??^xJzWI88ft$dE`d#ojnZ#);Xp8!R zW3cKpx%9Z>w!Q7Q%XLXd2QB80u^XhJ@E;d0 z9Q*vmnEF{s952#WNC;CxsTEHuN>(wZD25iLX%jUAdOe#nmj=g)68J*3;!5=~GR?q> z@FWcr|916i$EOarA1R(MOh^Mawgk$m0WuyBcekrdeO_irnvIRB-e=P;{R-_)$pX9{ zAxF1~MfQEuwLi+;BJ7;qYdLAqeB`KL07eV#HaA_ctv=y zZOm~XBuR*2we1Pl&CEHhw$_Cg_hcjS=i_F|w*5J)eykCzH*`VjghgHd=3R_|K;np^ zu`|4^07$?kZAkMx(_QSZ8DmWlU#V~M1Sh>b&L$b($79(CK=Rvow^boi27j{@j+t!< zE*o1fe>V)|%p&kDwO#sMBn&-M2sduYz&Q-1?pDYX`@<`QSx#RLa_z6a)@*5a=z-Es zpS_zlqkQafHEFG`S$NNDT!v0hy|a(_rjf6KYMf$3?%HN?_jy%&D_FcizNEWUn&LP@ zcGY7%5k7SunhuZ%pKQKxno?)w*8sdCtrk$J=;1D>OuiF#)dbewEsnz~w>AU(Lk2H; z=qK{xo4wip#2)9bxtHmw12L1_E*-b-L&RZnsx3{e#m>ZOFq$mneQF|=vWLM|H|Xj( zg+j+!PC7tS_-CQgtx%{?-TTy!rXWC*msiHZxfaTnyItLJ%zzdc?(~3?v2Tf8=l?!i z3q>PS;UV~!@jTWOvKL*GX3cl>)1xDpgd$cDRjZwDbu@*}n++A|X;o#oOOlku^l?OW z%xCwmor}&*;a%E=LBI+~#iU*yi$yJI5X5RZ+j3l=wsdu=z4eQnN2mR}gSq=cQh00prjw8p+2cO}y z6BprS2TP7(nk}zQca1ap{{fd#Ie+x>ACJ&~bm)7C#_;STbs7F=_~9r`hd_9(t$0;W z%cH&jq?l$hyeuxU$RV{#L&{1(gj`~=0Zg#NfPmOV=TH!42dLM@igl!y#ay>q3^f3Q zp|PW+zY>mF^Gl>DdjwwDsQj$I%Y0GO+`U!DEpHFxA)?^-FV-(k@QNHY?`D=?DeH@@ z`W;4JYw~-fE=;7yg?jgV9op1F{qjn}G}rr=^UXD1CCU840-t}*EjWArCx%+6BF!hM z-yn!lCML9H4g6+P76>OMJBGqlg)oyJw)}=srKB5 zXg)63Yx4M{vx&^Rb~}C6RtcHn)ewUvM!&vh ziFsdrpB+D9IloR1fqD5;T)oqdT9mwF+2H^sfE@~5*?KDEsQyNT++%ecuhMx_8}Th{ z*P|fGw54*QNy`UL)4+3}-`ir%kZ7i_8fl->v{0>=-5jOR4{4QVPr{(WmN6U+6|tk= zsB)#8z}2#>S6d|Je=j(zH>dVlSfNRJmizE_C*L({jM+QiFGahub;N=$4=DF!sZ;;) zLOKH6X{46g&_2fM1Le;>OC=z5ykFi@flhljm5UD4XC4{EZ+|Js)p~76-;Ob;a0S29$KnWE$isN=zWb4%L;;; zvEF}MZn6=6LCkRZ*m4+Az8C-E_M*$~1`SJ}bI*Sq+j~)2hR4Aj*T;;L7}EG{^wkG2lRGcQF58%3ZfYIlDd7y_xrFC3%i4 z+wv7G82oEDG8!kSiFWCk{d;w6DUbbj`z!m)G)=XbbZtqlaw^dUyH1G1V4-_ag=L}Z zNk65N{Z{+t*5hS6RmbCWMINhPrEg74uYHMH@M$FsVF47pi$N|j(ibC1b&Oz4?1x#^ zKVi4YY!Q++Fe*j{kvUk>r!VsV8~3D;&^3r>&iAb`{>lh+WReU?3BWvMDC1ioPZ%58 z5Gx2nXLiPIMoCaiwBa4aI;4pz?75dHiN6{5K2LHP3TEoL4| zj96dnU!Eq8&|)NT+XzYmdjduy6?Fbu06vIm9Zfmn#4fgLB*`@ZLlK8ft+2eb?%FZb zI`voP^AYC!T8^PU%wy5-nDItJI%e1Uu6HB#FTU@emm;R!_f`okR`#J@Nk4sAIyXh) zCZDu;|Ft!V_3djh?CS`DkOcs)dAcJfr*zzk6W!vqzG;<_6B^{vM-s+GoI|~_tDi6s)$wU@5BXz) zwrMs+jea_HADK_9j_Z1W*Uk^vsbPxHB*XJ)C}>1YPGkbr1Ll!f7?ef*>>g z^{aNYS}3N_-`@h~JvbMGOcjk=*!M2H%9uF66k_E*X?#{l{FKg=n;Kd@c0+9vRTea) z7MHqx4MOoS6fN;QH)ZYL&$|1W?sYSNpU$1?lh_T&)B|IpJFQ^Feny|Qds@HgwLtRC zM>J1=#HVUmTx*9*EJ#&Grs&OYfU7WFspKkS4uaylEgOVGRD5QOw4Id(u>493y9lw3 zm8L73blMloi-tZheDx1`H!TcNl-&0GO0XdlCKLuSGKxEVD;Y(?K+4xg%lD9%wEY)H zevR60_ZGKS*bE8A=Lm~2i}PrQg2AcV3)^n>V22nu?2ODp49qjg7+(n%!e2IhT<4WXF8o9 z7>!@0RWgRq`KYd;Acp6D&mMQ(HcT&RWshy+#^UwF3XK!Vr84VdcH4EayPW4^;5(as z#aTPq=CV+09(4E7gvS*(2CP}n0}Pb(uJtZ6e}^Ue7$(0o(6%sSxW@u!WR=U?f=s};oqdkw++A( zQ6mt8cWvZUetUzJ3q?&2hhI{~*toiS>>-nr4^k9Z_D55G?En;#NshZwTP^fTk7O-$ z4+AAKuY_Uy4ZA#eb>+qWG4hM5=u73NEmr{j-Nf%b_V2s9n}!bN)%L4z^EO9}y!NYr zCd6u!&+SF;;xH-C{9tkG{;&eyi@ZA95Sw9C^1bo=_Z{39^!!LkJSn34ltbD-RDvh;zX9E*ZjnzCmcLbT>LK)5= zHZ3|izw@Wq0PPiLK#g*-H2~Ki@7N~W5imLiDZb198h|dO=?of;2-@0;Qz)P8oFUi{ zi~$SyQ8~{v)YkHFmeYZUQS+Tm$2S-7KnK!X=LbtU^)1fGDoVdc4&1w7#t*R=8~Fni zugE%k{K%cAlrFl-pA}i!Pw1)-<9jcjz5W{r-np-9{x2j*HqmzF!e{j8V#L;4nG2*M z{W*vifx-%=;?t?%XPr2YT+mU;@)R&g&c_>ELDVTR|D!a{qWa<}x29^l~>g zM3}Tn-`DDu;~;BMdioUe-g0mVB}hJTO#j(hz4r0hIq*>dI6VlNJEnsu%>?k{7KPP^lQ1lK`-Tsb%$tDAKN6$%?!;% zzkNg@d1Z;mX6{8!!5>;J(304RFkS9P5-+4qw-R;>?m_f(F7B8Ea_^;1fbmZ^CeNDh z`ac74FpfNr;U3`c6ZDt1=GdD^V`=}p@!!8B2lOO;Z?`8PazM3U^)uy1w-S+!yyD=x zUmkk@+^Jw5VNn$O_=>tTA#rNC;A!wjF%vN_w1I=x-#v}?fT%+zJ~-)s!YmKlv=h$% z26;L?{#B36iuQ4YBx-DQx zlYsutt0Agz%^;zCi9d)#6Z1hNs#-R~TYRM2oW%YXtLsSe)tIjG@o1*tVjhyQE^#s% zi#qm5md8m`t`q1kN1~M5`#em)b#s*e)RR#9W0b;?HBVXcE=9)Y3?66mL{%mYP~@15 z?7QRmFpEVyl*zg{mSRWkWG-v1S?h*nHHj|OYUX$56i9i$VpCWgU&X4McgU-j%iWiz zs^nPJM<+z{j9^%-2_`!h_skca{uW=AD$x6t43BO9h0W`vG^X83ulS&~pO>U3mT`!@ zAXC61D_JKMffQp&o>B~f6cJWe|HqK2E`3E45m>2NkP@Cu zG3o1bScBB{$-OB5f;M2oRe0t$My`!xNr?-SW@9|&xiC&~bab3)9%*B(YZ5IFDI!Z* ztaJJ!j)cMHvfs{9$K)`w0oaCG;zhvL>WbQ`7LO%P+GEXKvnbnc9D5FCQyopxFZGB? z_F&Z-A&y=_Ta{YF_l2FOQW6K_evks8TMVo8jp>v7_shH=im*lJ?>;a}42r~u8|YQ% zwdbYz0KZAPcRyv270=Ta?Hkb4%YB8yP}lA=qp6ocb2y0>9c$|h%WA7hb9|y;TU%rEYkaLA!kf6q&@a`EmOdUjG7Omoicd{E-<^(Yu{3@xReizq?%DW1H zAmA_|rlU*{Jes@xJOccCvvWtek zgZ}nuf7gCRs=;yKNTK7%JcX}a`nYi9V!a_EX|v-J_PGA?%;<4c6;z7Z&&XxiUBLyr5OY|@j_x~*5B^hkt|vh+KZx2qU7oeLa{SCfr5FrRk5f7rK#9@CIMyAV^2lw zPe^`Y>?f#}kBM{DLNu?c(QcOK#Q-0v3cy*Z@dm<9%7EL9gdwi`US4Cy+Kt^Xz<|Op zh`Dcnb)u6bff>3BXyF<1wlm3nKX*IK@6tdE7J5%mLWn4oYLN=j?!NyQNt*U|%AE$A zYwwx>`AOdAls<$W!O&dz`CQfhYJ$}Mq^?7SeHkL;vxRrDyxRVWNec~H5t`I`qx9kA z{*4s%!slo5{CCwvzyOjuL;hKasEpB^>?-I57JaS<;=r_UgaSW3Qb`Xxg7Fm~H#J5$5DB{J-YT`m4$J z5988}Fgm4?5D=t8V!#;Pozf}N-7zIcca0d`9TG}ONHbb#7$_+q_}%Bf_&(?C=bdw( z=iJXbuIqJ81vUhP@Fk95C_w3}y!KhZ z*$@A)Rv4jPhP_@PB*3}dT+rlna2zawLZf@=k2=jF;l z{D=NBt&VOwd?W>iub(-@qG9?Nb2h>(? zCQRjy(0ZrqN-US%z4Agc(wCB1hXc650xZ{IWFG-LvoNU_dd=v;1s_e*gTdW}Ra4xR zn0QUhTxq!dsp-M@`5-2Kdx9I7qlkv6!@!gJSzyUV+PiCy<8UboWR=Oa=)}U(dfpYf z?v8wCt!AD`Vv?A&@&J}{VX)-=>EuGsipq6Yg%4Q8yW!^d_sRla#6W3ahu=ENcJ$%? z+q|voyH9G=t4zu;~kOHaR8Fo)p1DDYgsM$n^@WqzMu@BC)E4r zMnb=49Bal*l$lD%EUxF^+9LkIw3-sH{%TT0;*B6?iH3cTrRF)&&$irt=95! zR{=t<{lts+a7$Yd?~T;-7-_cN$L<*vQ9Xkx^>o9Nn}I;*WgAF)O>oOT&kZv&TC0nH zEd4g-fG><6*ljh4Ua2#xR2@~Y8-c_4Cl?IoR0TFzX-<()%`!L2@p()VVV_EVzA+ zX5``N(?66^`bkQZ6aC>ViS=X?4f0>b{X!4^Pf^B9>ci23cA93kK#7LrkpxJ;D6F7k z!1{n!wo0t;57f-n%SRwpql@q8c|Esj8h2q7ya`)Bh|Izp!Ss z(aotL+JSkVV!^T6gArE5xlkIgV1NEvigTst^4MJY&_ah@*}Q$Vu5XC{3s4ngZX0gt9w_xGs`Mi zj~-qXQSztub^nJEJ%z3nkvlIi57;PyZVq{MoVZf>_ar=Jg>!=r z0ALph>;pnV1U0**v2ng#BhLDnz^)l1w#QqD>e05tkoKzGKAqb(?ua&KmF6OGGG`l@ zPhN0obnhk^wpP|!rs?;S`h|c`P%|E49YBhsM>vpK7etP}ctWlKhbli6XqU zc3}+@8u+9)8LlAd+L7$%3=Bc#Fm6`PoP|OTb+>I>QK^Q~A6s4!Tkm!$(JBfjzX2<; z-b*J56BQ^=6t`)Vq8My1BERdeP#d9!|IS2G%UZ9Fai@hvHco%k=>>imWhHeg>s8ig ztyBRjqD+ei3CSU+?`|*GjpEcos>M5!>a^xuGV;%ulXNLQ${NP|16g;YuQ0C$vOSIb;k;@g?aU8)kMhKGa z){a0eFg`?6`)sVfy5lMkJ=|KVI_Oq8Z`!ncTX`EEzJjlE+*danGI_0OQ4uzY->3A+ z*O|mA4%~~yPR+?6FuFO>JoUSA?+(-K380b7~ zZ=Gz!dLV6ag=;m^eqdjh0a{)9-BM?vsG9aNySCncvpfWqYwluc*(5X;JS+ z`cWM9%H-YTN4FX2TeeWrpnTQ*;+xid!@GTlnZg3Yi@N!wPK|~rJ?t(KV>?F6iJDJ; zKoTzboc?}d=p_b9i?rA+3WvRZsC3^T(su`B9%bI#CQ_llPh&o7y?zh?V51?;FgdyO z<~A!-KlmN6*_E)(nKQ&(PU@9N@Z^+{0O*(Z`}^*xojMq~)vxBU^@|`Qqvj$5|k+(*UR?iwdJm8!IDgz=$8^2m9 zdDnC~^k{o&z1|eD%C7v$`4vASBOr?#&#bawh}0n0Q6P$T8r2rg#*rrhr-WvIC?Unh zB4^|4IyMpT?Dz76If@W6$-H$GOaKDlaaPv*jLKP>>$^#i zzK|)vWz4(mw^}i*h>Fnr*P{1d*hnd9YW`sO<1@)f!{3x4 z%?=mcncs%J|HC(&8xGtr7IWybmyUX%>{|*N5=U&6)4~~i$EGr7MHHiORN6JW29?Rq zWjms&TqTVoC(DtRR|AWb#|?+ch=aXs8bz3>ojd8^{$4w)5tIX+2IzCj`+vHY<_GH6 zu35a(X*REAYn3gphyj}sw7oN~#f;|;NCiyUYz(Bkyp-cxM!veIu1LZBp zh_Z(f+(zh)J{ksbte(n#K8<}Qc-@q}@aXMp9ilX>_ajzzTwr%$xcl3)!7p|4#reve zn(XLpVj3yzTCT<4T2QecWU_@KDp*Vq69;+(_)aFJQTzNNSj}hb85S2Vb;-Rg-T3QK z`pfJmBJD3dsU(4SjI;Z}VRzh^Ckp0duxH+(hb~Zn8T`EOM zq4FDQb@iRqH~;|PCQHPDlT*ES#jf%{8i}0VNX0x5LfA)72^OShwet0%08+lo#CzDQ zBP9%bmL9#ozlJbrFR4pAY4AwVr70x>k!JDF+V~ix$ab+HrJHtdp)al84~+;G_==4O zK0`n_X{onH zfl0z25zB3MIb?1Luv;x0NUdUUAXWxRtt35)^9fNz;f!tIKJMpvSX7uK{CtgvnipY- z`-Gt&w~W(*Zm}ZN_q_f@5ki*V2X?IRn%TH(glO0?O2!n@K@YeYN_y6P(qrVe9yR#G zVYZTj@CE3blrBZdZzDr|)OK-Du7jsN{;*X;eJf?BgJzX-#d(L{9m+34uEI1?dx?jD zq|oNz*<8X8_FX`i7z$0Ti14z-uulrr<;~|KBJV?3emQ-{>3qOZ02Qy zL5Z>DWx^M+90oOd%4=11W)8&#hPh*MmH9Jxsw;{%1)soGd8n%G>D*v(B2M2&lxfi@ zr4le^8TKpc#os?DZ2Ui#h{tgov}GglWN(1ntlfQ0Vcip z^ue#FhObiSTD*{%^n~cN6wy8)m-owrISWVPuhN}JUr_{4L4|#-9=h-ap}JM5B?8;V zz6>|6I3iv8;qzyAf_ejZM~=y{Y}3#dy=>g)Ya`L|H^1alTp{=V|Mu{( z>gkYH?ep5i;Ao9J7uFZd!q1Q^7IyWRzgvC%`JI&?58IY49^z2pE*)Z*oyjTFn`3Es z4R71!3PGir17rX@E4h_VsfU@S?ng4Rr@mqp(bE#aS37;wB@-s4qC>kT?S$2-oVa+c z(0dWR4HkV;iy}Smc1!d?p7t@fqD|{cTjQN_oml+iK)UDm)pje&GJN=55*57dYT| zHyXsf|8AcuP}n`o&z;2 z$Zb=2A^!z%>JWQ$UNSvRr-WE`Ayf> z)^H{@qC@LP8~73uhWf~((F8dq3(6@~@tVp94V<#+W`-PbOMJ>t7E<@RU{Tz*0z6TA z!E}!}R<116Y|4krJ-y6Q{w)Yw#7nKjaldL{FiwNwYw-A_FxVa!sMCAucq&rS{LaeY zB^c~av1Qf*QADzT4}W4nf}g=^$`t(AT)%WN>l6Vuri_aOtkGFFSZIowp2rV!Cua+N zRDpSSio~%o=~^P-gc-PPqHsY1T!Oy?CZ)+moslI7ax|UiB!B5$f}rKIPIjh=r|3Cl#r#i zG2p(9K3XbtyI|#f-Wg&5J1YeyIP<0JSZfXi%2m0>iIaEma9@fJk?;u6Eakh4y;6X9k6>GEq~;hK%TH00*z=#8 zz5||UOr6m6r0Kb(*i4?1^?!ZmEI9(fa&kmXJDla^7pHsCoW?+loS~cw zBn6>`$>XZ3NmA;9!h+XO4h)RntAT$c;Vg3%Yxpv9-D$F(96uy#9|E)8%^`gR^5J6f zLAG?nuPNz{|Mlx+w|>SICP@3dM9DFAp8&F}VsnG;eQ{lVM#s5Y!&^`sgx0Nwklid} zoD7e76C(bY2f{f7d{rnUr1dua&Zfz#)yM(BN@?f_>vphbUy;AS3{fwB5}oYzz1wiT z2yBo?H5%()|{$2Uf%8c8G)<56s^cHuYV}C#Z**+Td^tF&LyX^@R(S zSt!Xv8GFd!f6$Lwuv;>{CJceBDtZb4iqbc0e9ZH7(3q|ZN72UDlym|5POa4UA3
    `GWNN7g}LvhFP4t%2kMfMN}pot}qB?G!Doy)xk<)l}X<{ zPT_(<-W-p!6nOE@31|?Sk?Az9Xl9Q4l8H@-eW**XCmb(CGJmfpP`f9D%!hKask3QZ zaI(hC9npE3Fj`=PQz{q@<2tyEQ~dcz=(*QmJx`9L7rQjF#U)W%2KYogqnI;&t*C<7 z^o$3cC?cyK{yxf${S$M!#SuzKc0`{rTZ-zH;|{Wf0Oj6{yYMhCvI>ksemAvb(^Aa! zSe%ZK7ixy)K8yS3VgDXH=HH2A#WT*H|Nfrm(SNt1t0BIpxFssq_6j)?WYB}>wD6ZQ znA$Rxyb9$A$*^-!?emrf%iS+5tm~gJ_MrOr#9}}WR^nKyQ+6W?3O+`@XkfxO=~{^j z1U#ww1%lg`4?ZK7SPd%Gy=ELddfj~rbCxU&5tI17f)esyVzV;hYJ>(mbr<%v!Mx2riDUNB`0N%YEfZCy9tNnSYK2*ADCa+Zejt4=02XvQ zTGdZ?EiNpSeOcJkTB#A@F^R%eHTkwa<5t-Vc1+IE#&(0I3Clq)@;<%@MY-WE!9fJ& z+UJgXtsZNY7;2n0$_kuiC>$015GzK93q?>EU#(iw??EjO-V3a|7 zy#XQR*-$@umh$2gTolo9!7S+)ZU(bqLja+Ii5P~zPY&JD!*`hK)0Fg`-7T7IzAH}u z@XoO3LvV&StY9B9|SX3w)8Jj zsUoG--EVdCH}0ECI(9mT7qHDLXoQ#SPCpl7Dpba#;fK%6e6K;JTF#G&RL>df5+}*H zpUg&r93>a_#ICDZc3#fz`z1u#)42{GTL@;bj~1j{?t;|xCz)cqZd(=QKcw+jn_oeD zTX4$w6plx3j&J}r7$m5+D3m{4i$?8Gy6c;!ci`VC4xga&Jv|D-X2wZ6mIOOcdsu znP0G4`vW%U!v_>#aNU1|R5`z4=5j+YzQpbL>EW#BoB#(!bT8|k_&jNc^>b~wLc=mL zGsgF1@@>VJ9t9*WxA{H)y&x*l*J?YY`}~b+1x5r=4pnU9^}%(8G(eQJKc5r?h4uMu zCq&XBzxhrGa$8OhLa=|lQF)&21+5E$e@m^~pu7Eije+iKh3T<&N-e|GZrasxx-F|O zPE?)ivNT^AJHLog?w%C+nMYOAj)iMYZfaJ(UlB1r(?BMO~`;T|Urm3+WwQ(!Z7 zZeiH#WDwDA@HNvE{jsnY_mGm2Ao|UNWQN%*4QR4x)0}-pb98YqC?U-Vtsq?7;BLK# z>n zu=D>Ia(K)&-PZ>zTgDl(L^4=gO32Sl>Y-(km?oJ|&m~_%ZMLa1FQK;62P9uvv(RXT zO-_1S^5{CZ@#@e0yV|q#Q4R0gk0oi0RFP^qro=9--1a=z=bJ z6QDauOBuNULD<)a-5*tk4WQ(l>pYuWV~FR-iM+skbMgzd;6?VoZ{lgb_(g~$?k1bR zQR}y&Nl;@Uax2{lAtX;mrRKCM&E_S(i58aO!PUr4FwE%m8B6b)iH(-Cj2yW=t8A=< zuG1}kN?fUX_uA+s?#Ri;@qkYwPyYHjwjwkq6K%Z|P)H(_9pR#7r)<@td{dvZ_baH$I+((OS;q;@D&E}_!pQ+Se_BSUNr^Kgw6RssV7%~TK zoYfH>;tXvJWYEe-zBOc^{fTXWdHg(RZPTf9<`iZ|h4;przPUBT`Uk|-WCT30YzTE^ zzxXCWeusc-v?X6bkYq?aU#-4|{AK!-FYwg&Ob84g>j2cOTfAYez2i!9>rR781}i$* z8v8Y;v|F5=3|4Ryb51kAxBSIPlK+g@&jeD=0&jrv@MihB^_GbWst$>us)rt&U)^o} zL-m8(ek-!8X9|1ZP17y-2l#{YK7Bhg#H32qk*Y0Qn|KZ_^t)a+C8q}zPDKr@CjXTL za2knx-HNo^CM1tj)B$m!y(^b)N?hipzDGKt&17RsuEJ1Y1#$4$ih25<4}XGy1Ym0aJj_E&WEHh7dNV$7QDci|{K4exmL zKqv~t*T|u=pjUUI3d?XUVCGKc^nz*bN8|3qd_t}|_?Hq{-PX<@e0LNH%b)pnldu3Q z2(0nl{WQ$-{YKrtE-}wlVBA+)CT^ddP1mw|*7c{8HSWKsIk}`K`)O!K;}oaBIF|8n z_`@`}9Rb0hj3md(xnSZdxby8_B2!B`;i{Rm{HIt;J}ijI`W;6M!ZshjG| zq`yi!a%DdUhgb=2vK%@lZ`r)Vcf0~fZ50t^a2ce()ITT{unP8`kuQyP_ z5ShK8BLL2NztK+PYRR-y@qkW;i^mERUAe=azFZa_9}Mvak#NWC-XdK1ha7P$ol0AP zNCMR1?d7;FvgxH6v?=Tcie-8;)#>XL9v&BxO{CjnS{RpPDv$+DTG8|75Y{iO@;V($W2noLZO#rXGVbXTA` zjyx+dRD!xm!Y9ixq#VM?t0YXXcz=@h_xG8YBx7`eGOkQ<_~#9u-jAGkVZ)z&Z*Wdd z{Y17=HWXo0Se9q@?U;9$?sZfQq3#DFWM4@-xPmQ&w;+(OdbTux!M=R-S}{}LD4RDx zLSP)PRA@X?{1(lLHX?Qxa-2!LIQHN7eXtu;VoaSXADWR_BaTh8#O8ypyLKO>&MWuU zv%R7}!<~h-1l{)%l`0pw+2$SOoR>NH1pzHHE;<1qvrSjCQyu3#2J>XV`^f3y8Ou@e z{U)ozGf?H`_vx6?Xdo@(T(Es!3xh4fI}7uq+-gI|!#-KFC4q*QjR$m)lfgI{x~3Bp zyNoN8eb;5j?Zw#Q`jg&L!9XzQck+ogta9xScA|0l!9$KoJk5l^n_R^O z)TO_PBwfNz@RPl2)x`iSf}<<>UgAq*f=nv6L4RZa&5opnq;{!$M`F4?$|FV@fG4Vlz9U|Iy$;|MvP)Wh!2@w0QkR=Z!3u>hduZqJjktbc!qpRWNj z$d$6N2Sjpm&fV_?xU;1%YxHOoDfT^usCU-~3lOSfvVl9vB3iJdT2##B=;nOe_55a* zeVZpjCEuI9J{k=n=%3?RJMd-d`st&{_Kxg>MZg&e zS=v87RXLh|DCd+Gx)5FH_zljQ`HF7m3|Wool86pry`iryob^H{oU#z zr7U*rAh+Rg(yLOSN!d&N@n588&!1o9&nsiIu+8ij1s5%ZE6kr6z~t>xsT@xA@AfXAfj+<{?e*2K0n>S z5rOE&Y#g~SQP5_uRdwoYQxt}$1$?z0yCCJH8D$17={J85A@cVKG8H}lV%Po4i3E(_c{Uy(b9Fi%<({z1kwP4__=@4H>8*L4>UAmjyzn ze(e>>(eiy^V)>i-7|w~O>yAE2PA;X~^k;Ruv)N0baqD2-_aFu?g}lR@fydqO9PxcE zXeU#>RgM){s-vf!H zkVr7!1%QU^vTVpi^jh?7^=v4H-Lo&5|LAyd-Psl3pu?F`!%144MaNfy|2{^K*Gw)L zx&b6zz*#V4H<0l2f0*Arri{pFAJJZ1ekR{tO#=ANfmS&k{$%J)4X16 zdS3Zn^qqKQS9@v&X`^qoIY~V!2k@CS6p{3kJGIhIe_|hv_}`LnY8CTIjww;*Aon3} z8t-6iPW{l&YV$OWJj~}A@o52>QYw0b`GmlGz5?=z%h9Eya?Q%2YtLe}fOGPt(iyM4 zj)6yHl9noKACqgcLFw#YoMy%|=A1weU*%$Joirw4+i5^5Cgj>&qy~JuJMS#W2 zfFBM8A7!(<#@ch#-)QY+s+8VT%X5}R*w6_BSs0#Gm2M)L;RZB0x1ITV9F^5`F7GX} znM!#xtmj$+)$b)Kl!^f)cB|53&KGSzdEq|URi&ec`W<^H;qCcC&F*ULaRV~~qE#kz z?pGOoEa|}7D40>#aGvwE1kpBZq_q#qVNNxeD79@WvDGcrjS{0l#s!2OHCC&XV6#p; zi+tx@GLQ(7#>FFxR=IETsRpmG|HOr)P177Ovud)b&ar$%HJ>-GHsM?vvvwGaN9_^& zshd|hZAbUq$dsn28^;shu<{Q$8O;&a*iL88W1qnk1`{Nd(a!8L#0Z$Km&FGS6>CM- zcGC&9o>85YpR`63>T8ts4#}552c7rHKaob*(tkC|Ps+pmv7mw%wpJ zDPo^Sarz(ejyf;@-3_*f{3{m@{zoC~jzNTaC8@y_Q1P`7zW)$Rr~ZHMq(gPMs}di( zj-EC_Vyxwv1x0sb>-MJ4%a6gR-5GnA@Z^8`*%Xbqm*b>LQ5~GSd-+M}SNPVBgaSH5 zx_3<*KQt5oZNI3}q*mQ8Q-JFKtv^0Fnf^Vk%Mg`b0)Ek7-*H^X${wob_vITI z#yldq83gvS>9&v1VcL_i#a@j{t-Gf&@YAfcH@*}HRCFAPb28W_xUPD_WcX9?6vD#o%G1)=W zMx0{T&r^E-FWo;+^BT9>AVFWA;A=g9B)>t%KnwJczguUgC1MUS)n@0i)YYDAG9C`GYcM_f!=|!y-zz-$ zVr#Fmy{5jqT1E07DZ*3KDNemUE)WL9)y0Dq+hWJ%8o~&~;xOUurE3hvHep2Xu5kOT zdo1I`JfWVD+f>|OCDx8J{obR;-)j~`&ian|Y(IZ;%@0t((Uhse7idAT(ogoM{;m%0 z&W8K7acigkJ=nK9F3|1}`*{o!84O$=ytsuU94w7;m2YJY<++?URD!J`_)@8mgD<}) z0}?L&_RbqjgGZ0VT|K@#H2-KRdURbz^qx37o5NlEU#5XoLkInC*gpvgKNP}Wg$((m zeKxB~Nadd1CLvl)4L+WP?zJeHEOz|~2rP0HiXMGG&?=-`c~Sq1w*B4T-w^&xx4S0Q z{vq9aI(nBbIMT0nLg&xgQ{Wl3>fR9*C5{03`fi3%s_|+M zBFaez4Q^1C_4rGLs+%@9lG;_x2zc0|kB($DDP6UdUZfa@T&pX=xUV|y`0BK1w#gEk zLrsQrVdc(N<#O>A9R}vXGri+!X;n`n?sYLPr4GrUN;});fzh{gg&nD{4xpS}`eExvWmM@HJ-+1FfYl)AiG9`WIhiGQG4 z*SS;rZe4I5^U)+U%C{S)jHlaX1lOL8^y`^?dc7!<&JhlUyLiH7JBe_LdNT>32{>mo zKl+?wMbt`#9M$Mjr*98`NvdWL6W7k;)dhIn1PR-Y2mJ#y&y|l-PVp>%IIKsW$LQNP zH#!Sh@6oWnCP5g;fx7pN01}naYrH$awhIp>=-;JCECQXj)?`2Rzp}%)^BFv4!+bte zFZ@G9?ijAsSS##JuJ`_T8+8Ajipj29kUmK{K5+IfaJ{bw3)TsVnt*=q_mb(|I@ z?reQ}o!UvFC56=}0V&m!n-m6{O;Ssl1GLfNwZ=31dDxpgVi|?wx^CmZgzOgaIwGZ& zo$kSUvQr*oJ~zu=mOdOSWT(ypp-|EtCeK+a&dg>$nu4Tbx9J2!(gbX7?wJ~v5$cn{ ziY=`nbGLgh+IOTroDSl`G>^j|TfU=W8RQG4(oJ_3DzgnWFwU2$URjqsTVjy!wkl9%7kf_w<#jpVJY43r#&bg%e5- zIWEY3cYQ^vuIOy>MdpPM09sVwft>7h3ON$|ZAU);uKBM1RY|XAhU54iO4S6vasqT) zisXIBITpb+_sRqBmH6vV$zqrTP&Gwje9GB#6UpSis{DNmRnd_^c&diKFi@i z8*Sz|#+wr!lkMWEb%;(%V>kP6@|N(L32E%08MpWZWO!a3v3G$}cU-kr!|lZ)+#^Vk z&cAj>?#U5{(Vc!K)!zJ9UD5xL&C@eu^QzSvJ}2cYf`GO}8legwlu;~6O4MM~_;_Qu z$0Xa}n2-#iWjxJ6hX>uNthQv$Uu|(X;6=U|%}!h#=gNsc|1LMP_E_Vb_bft&>6 z^0tr$b$l^C-g}~8@%9wVDGe8kq1~;ioJ$g7RhfVYQ;y(~>V2z+smQHXJ+CK3k{Gkv z4^rksaI}-a_!3NGY##9PrG$4(4q5m?_fJgx_9 z#)egv|GAto_K(PH;KdW(U6U~Q>gGem zrny_Pkqc4*!#}n%zs|zsGeg>F4dW5eUvY38WBV7Y&I#|CXL;`s;*{skjLin0*PUS| z7xA(euS}k{w45ivV)p!YG|H?+yo0GCgHVyJb;*{*Qi zDhC68Bx??f$Uj&bT+>xq%m;M7)q~hCPwhMVDyteg;-02*4JvqB2LNlJw!f=$ywXy} zhFuK0Idxa>gGZEd%M;ivEATa|M*X?wfXGmTry=WHc!8QTcav;WYA$a^Vg$|ROf%D@g5d9=IzwKkS&m)%BU329FH^}%PKuGg`; zpAaa=SJxpbs>DKW-60HG1=4gHjWdpawdT41j4axkjYV2#u^eXg8j6k2Eg%Yr3IEH+ zF=cGA)p!hO93nQ5jr8H6w~ih?&&geceQ6W}l00x*lj2$K#fH?2MN>d=|6%uxHa22G zE8Y2XF>~30ta^FP(~2}6>x}0x(TsChFL7pz3BFDFVoSBff-!`k? zHrS3&s#DM*zNK)#VrNl&AJ1U7alqm`t(Q`-fduczeECG>ciMB_AA|b!9C}q&NbhF} zej&W4TckG~+Ji}jiXW+WPl$bkJ)E8sBjeF_y2YoZ3p*!lUP&8glD1|9wyb>Pzr$DY zttV}Nq-4vVdTGl>^Vgu$tbJxc`QfJyDu-&KFzR2~uvQ7%AG$QyhfFxQ{a9Ut7E8O0 zjA!AB0jigwDZKd{BEjuf4@yTG4Z_tBM;9FOUs>&D8sRanhJ1@M`JQ z>W^*zZCaHJ$xK`p7r!q*vqMm7-}HjOEe5ZH>9+2om~er~g<=dm3Mr-e_u0Z?U-jA^ z*rn5U;xJ7q^YCjgAgKEdc*Z=WMbEmTqea72n@?`G{P^11C*$7m33v& z!bgwQ!`W?%JNp4$_X>Mxml~$#oL3t9g&CiPLzv##%sC%QgBMiKsC?h~pBZR2<~UHm z+m{)v4|o%vVkR^iEi#;vP=mnaA$99)pvwDsjJdM&Jb+rntU?AcKZI7ygGJPBp}`vUMm z9(himR$CAOo>+=(%jlKA$PtkN(OWrdSNp&xY;zn0Jo-1*^0bU{g`_qLns4V+X4M$Vm zD{!CKetO`32~vN^G;P5!al2Y&sqb&az&FIB1@Qm-i5~lhGc6j?(>6O=@eK8-C~5%f I<*g$B2d-Y^E&u=k diff --git a/apps/admin-x-activitypub/src/components/FollowSite.tsx b/apps/admin-x-activitypub/src/components/FollowSite.tsx deleted file mode 100644 index c137e9f8702..00000000000 --- a/apps/admin-x-activitypub/src/components/FollowSite.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import NiceModal from '@ebay/nice-modal-react'; -import {Modal, TextField, showToast} from '@tryghost/admin-x-design-system'; -import {useFollow} from '@tryghost/admin-x-framework/api/activitypub'; -import {useQueryClient} from '@tryghost/admin-x-framework'; -import {useRouting} from '@tryghost/admin-x-framework/routing'; -import {useState} from 'react'; - -// const sleep = (ms: number) => ( -// new Promise((resolve) => { -// setTimeout(resolve, ms); -// }) -// ); - -const FollowSite = NiceModal.create(() => { - const {updateRoute} = useRouting(); - const modal = NiceModal.useModal(); - const mutation = useFollow(); - const client = useQueryClient(); - - // mutation.isPending - // mutation.isError - // mutation.isSuccess - // mutation.mutate({username: '@index@site.com'}) - // mutation.reset(); - - // State to manage the text field value - const [profileName, setProfileName] = useState(''); - // const [success, setSuccess] = useState(false); - const [errorMessage, setError] = useState(null); - - const handleFollow = async () => { - try { - // Perform the mutation - await mutation.mutateAsync({username: profileName}); - // If successful, set the success state to true - // setSuccess(true); - showToast({ - message: 'Site followed', - type: 'success' - }); - - // // Because we don't return the new follower data from the API, we need to wait a bit to let it process and then update the query. - // // This is a dirty hack and should be replaced with a better solution. - // await sleep(2000); - - modal.remove(); - // Refetch the following data. - // At this point it might not be updated yet, but it will be eventually. - await client.refetchQueries({queryKey: ['FollowingResponseData'], type: 'active'}); - updateRoute(''); - } catch (error) { - // If there's an error, set the error state - setError(errorMessage); - } - }; - - return ( - { - mutation.reset(); - updateRoute(''); - }} - cancelLabel='Cancel' - okLabel='Follow' - size='sm' - title='Follow a Ghost site' - onOk={handleFollow} - > -
    - setProfileName(e.target.value)} - /> -
    -
    - ); -}); - -export default FollowSite; diff --git a/apps/admin-x-activitypub/src/components/ListIndex.tsx b/apps/admin-x-activitypub/src/components/ListIndex.tsx index 7c109c8ba81..7156dba1457 100644 --- a/apps/admin-x-activitypub/src/components/ListIndex.tsx +++ b/apps/admin-x-activitypub/src/components/ListIndex.tsx @@ -1,282 +1,26 @@ -// import NiceModal from '@ebay/nice-modal-react'; -// import ActivityPubWelcomeImage from '../assets/images/ap-welcome.png'; -import React, {useState} from 'react'; -import articleBodyStyles from './articleBodyStyles'; -import getUsername from '../utils/get-username'; -import {ActorProperties, ObjectProperties, useBrowseFollowersForUser, useBrowseFollowingForUser, useBrowseInboxForUser} from '@tryghost/admin-x-framework/api/activitypub'; -import {Avatar, Button, Heading, List, ListItem, Page, SettingValue, ViewContainer, ViewTab} from '@tryghost/admin-x-design-system'; -import {useBrowseSite} from '@tryghost/admin-x-framework/api/site'; -import {useRouting} from '@tryghost/admin-x-framework/routing'; - -interface ViewArticleProps { - object: ObjectProperties, - onBackToList: () => void; -} - -const ActivityPubComponent: React.FC = () => { - const {updateRoute} = useRouting(); - - // TODO: Replace with actual user ID - const {data: {items: activities = []} = {}} = useBrowseInboxForUser('index'); - const {data: {totalItems: followingCount = 0} = {}} = useBrowseFollowingForUser('index'); - const {data: {totalItems: followersCount = 0} = {}} = useBrowseFollowersForUser('index'); - - const [articleContent, setArticleContent] = useState(null); - const [, setArticleActor] = useState(null); - - const handleViewContent = (object: ObjectProperties, actor: ActorProperties) => { - setArticleContent(object); - setArticleActor(actor); - }; - - const handleBackToList = () => { - setArticleContent(null); - }; - - const [selectedTab, setSelectedTab] = useState('inbox'); - - const tabs: ViewTab[] = [ - { - id: 'inbox', - title: 'Inbox', - contents:
    -
      - {activities && activities.some(activity => activity.type === 'Create' && activity.object.type === 'Article') ? (activities.slice().reverse().map(activity => ( - activity.type === 'Create' && activity.object.type === 'Article' && -
    • handleViewContent(activity.object, activity.actor)}> - -
    • - ))) :
      -
      - {/* Ghost site logos */} - Welcome to ActivityPub -

      We’re so glad to have you on board! At the moment, you can follow other Ghost sites and enjoy their content right here inside Ghost.

      -

      You can see all of the users on the right—find your favorite ones and give them a follow.

      -
      -
      } -
    - -
    - }, - { - id: 'activity', - title: 'Activity', - contents:
    - {activities && activities.slice().reverse().map(activity => ( - activity.type === 'Like' && } id='list-item' title={
    {activity.actor.name} liked your post {activity.object.name}
    }>
    - ))} -
    - -
    - }, - { - id: 'likes', - title: 'Likes', - contents:
    -
      - {activities && activities.slice().reverse().map(activity => ( - activity.type === 'Create' && activity.object.type === 'Article' && -
    • handleViewContent(activity.object, activity.actor)}> - -
    • - ))} -
    - -
    - } - ]; - +const ListIndex = () => { return ( - - {!articleContent ? ( - { - updateRoute('follow-site'); - }, - icon: 'add' - }} - selectedTab={selectedTab} - stickyHeader={true} - tabs={tabs} - toolbarBorder={false} - type='page' - onTabChange={setSelectedTab} - > - - - ) : ( - - )} - - - ); -}; - -const Sidebar: React.FC<{followingCount: number, followersCount: number, updateRoute: (route: string) => void}> = ({followingCount, followersCount, updateRoute}) => ( -
    -
    -
    -
    -
    updateRoute('/view-following')}> - {followingCount} - Following +
    +

    ActivityPub Demo

    +
    +
    +

    This is a post title

    +

    This is some very short post content

    +

    Publish McPublisher

    +
    +
    +

    This is a post title

    +

    This is some very short post content

    +

    Publish McPublisher

    -
    updateRoute('/view-followers')}> - {followersCount} - Followers +
    +

    This is a post title

    +

    This is some very short post content

    +

    Publish McPublisher

    -
    -
    - Explore -
    - - {}} />} avatar={} detail='829 followers' hideActions={true} title='404 Media' /> - {}} />} avatar={} detail='791 followers' hideActions={true} title='The Browser' /> - {}} />} avatar={} detail='854 followers' hideActions={true} title='Welcome to Hell World' /> - -
    -
    -); - -const ArticleBody: React.FC<{heading: string, image: string|undefined, html: string}> = ({heading, image, html}) => { - // const dangerouslySetInnerHTML = {__html: html}; - // const cssFile = '../index.css'; - const site = useBrowseSite(); - const siteData = site.data?.site; - - const cssContent = articleBodyStyles(siteData?.url.replace(/\/$/, '')); - - const htmlContent = ` - - - ${cssContent} - - -
    -

    ${heading}

    -${image && - `
    - ${heading} -
    ` -} -
    -
    - ${html} -
    - - -`; - - return ( - - ); -}; - -const ObjectContentDisplay: React.FC<{actor: ActorProperties, object: ObjectProperties }> = ({actor, object}) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(object.content || '', 'text/html'); - - const plainTextContent = doc.body.textContent; - const timestamp = - new Date(object?.published ?? new Date()).toLocaleDateString('default', {year: 'numeric', month: 'short', day: '2-digit'}) + ', ' + new Date(object?.published ?? new Date()).toLocaleTimeString('default', {hour: '2-digit', minute: '2-digit'}); - - const [isClicked, setIsClicked] = useState(false); - const [isLiked, setIsLiked] = useState(false); - - const handleLikeClick = (event: React.MouseEvent | undefined) => { - event?.stopPropagation(); - setIsClicked(true); - setIsLiked(!isLiked); - setTimeout(() => setIsClicked(false), 300); // Reset the animation class after 300ms - }; - - return ( - <> - {object && ( -
    -
    - - {actor.name} - {getUsername(actor)} - {timestamp} -
    -
    -
    -
    - {object.name} -
    -

    {plainTextContent}

    -
    -
    -
    - {object.image &&
    - -
    } -
    -
    - {/*
    */} -
    - )} - - ); -}; - -const ViewArticle: React.FC = ({object, onBackToList}) => { - const {updateRoute} = useRouting(); - - const [isClicked, setIsClicked] = useState(false); - const [isLiked, setIsLiked] = useState(false); - - const handleLikeClick = (event: React.MouseEvent | undefined) => { - event?.stopPropagation(); - setIsClicked(true); - setIsLiked(!isLiked); - setTimeout(() => setIsClicked(false), 300); // Reset the animation class after 300ms - }; - - return ( - - -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    ); }; -export default ActivityPubComponent; +export default ListIndex; diff --git a/apps/admin-x-activitypub/src/components/ViewFollowers.tsx b/apps/admin-x-activitypub/src/components/ViewFollowers.tsx deleted file mode 100644 index bff12ef78c4..00000000000 --- a/apps/admin-x-activitypub/src/components/ViewFollowers.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import {} from '@tryghost/admin-x-framework/api/activitypub'; -import NiceModal from '@ebay/nice-modal-react'; -import getUsername from '../utils/get-username'; -import {Avatar, Button, List, ListItem, Modal} from '@tryghost/admin-x-design-system'; -import {FollowingResponseData, useBrowseFollowersForUser, useFollow} from '@tryghost/admin-x-framework/api/activitypub'; -import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing'; - -interface ViewFollowersModalProps { - following: FollowingResponseData[], - animate?: boolean -} - -const ViewFollowersModal: React.FC = ({}) => { - const {updateRoute} = useRouting(); - // const modal = NiceModal.useModal(); - const mutation = useFollow(); - - const {data: {items = []} = {}} = useBrowseFollowersForUser('inbox'); - - const followers = Array.isArray(items) ? items : [items]; - return ( - { - mutation.reset(); - updateRoute(''); - }} - cancelLabel='' - footer={false} - okLabel='' - size='md' - title='Followers' - topRightContent='close' - > -
    - - {followers.map(item => ( - mutation.mutate({username: getUsername(item)})} />} avatar={} detail={getUsername(item)} id='list-item' title={item.name}> - ))} - -
    -
    - ); -}; - -export default NiceModal.create(ViewFollowersModal); diff --git a/apps/admin-x-activitypub/src/components/ViewFollowing.tsx b/apps/admin-x-activitypub/src/components/ViewFollowing.tsx deleted file mode 100644 index 4a5bc82cd68..00000000000 --- a/apps/admin-x-activitypub/src/components/ViewFollowing.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import {} from '@tryghost/admin-x-framework/api/activitypub'; -import NiceModal from '@ebay/nice-modal-react'; -import getUsername from '../utils/get-username'; -import {Avatar, Button, List, ListItem, Modal} from '@tryghost/admin-x-design-system'; -import {FollowingResponseData, useBrowseFollowingForUser, useUnfollow} from '@tryghost/admin-x-framework/api/activitypub'; -import {RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing'; - -interface ViewFollowingModalProps { - following: FollowingResponseData[], - animate?: boolean -} - -const ViewFollowingModal: React.FC = ({}) => { - const {updateRoute} = useRouting(); - const mutation = useUnfollow(); - - const {data: {items = []} = {}} = useBrowseFollowingForUser('inbox'); - - const following = Array.isArray(items) ? items : [items]; - return ( - { - mutation.reset(); - updateRoute(''); - }} - cancelLabel='' - footer={false} - okLabel='' - size='md' - title='Following' - topRightContent='close' - > -
    - - {following.map(item => ( - mutation.mutate({username: getUsername(item)})} />} avatar={} detail={getUsername(item)} id='list-item' title={item.name}> - ))} - - {/* - - -
    -
    -
    - - Platformer Platformer Platformer Platformer Platformer - @index@platformerplatformerplatformerplatformer.news -
    -
    -
    -
    -
    Unfollow
    -
    -
    */} -
    -
    - ); -}; - -export default NiceModal.create(ViewFollowingModal); diff --git a/apps/admin-x-activitypub/src/components/articleBodyStyles.ts b/apps/admin-x-activitypub/src/components/articleBodyStyles.ts deleted file mode 100644 index 4475737005c..00000000000 --- a/apps/admin-x-activitypub/src/components/articleBodyStyles.ts +++ /dev/null @@ -1,5911 +0,0 @@ -const articleBodyStyles = (siteUrl: string|undefined) => { - return ``; -}; - -export default articleBodyStyles; diff --git a/apps/admin-x-activitypub/src/components/modals.tsx b/apps/admin-x-activitypub/src/components/modals.tsx deleted file mode 100644 index 5764840ba39..00000000000 --- a/apps/admin-x-activitypub/src/components/modals.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import FollowSite from './FollowSite'; -import ViewFollowers from './ViewFollowers'; -import ViewFollowing from './ViewFollowing'; -import {ModalComponent} from '@tryghost/admin-x-framework/routing'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const modals = {FollowSite, ViewFollowing, ViewFollowers} satisfies {[key: string]: ModalComponent}; - -export default modals; - -export type ModalName = keyof typeof modals; diff --git a/apps/admin-x-activitypub/src/styles/index.css b/apps/admin-x-activitypub/src/styles/index.css index c3b58ac6828..d1f1f198edf 100644 --- a/apps/admin-x-activitypub/src/styles/index.css +++ b/apps/admin-x-activitypub/src/styles/index.css @@ -1,25 +1 @@ @import '@tryghost/admin-x-design-system/styles.css'; - -.admin-x-base.admin-x-activitypub { - animation-name: none; -} - -@keyframes bump { - 0% { - transform: scale(1); - } - 50% { - transform: scale(1.1); - } - 100% { - transform: scale(1); - } - } - -.bump { -animation: bump 0.3s ease-in-out; -} - -.ap-red-heart path { - fill: #F50B23; -} \ No newline at end of file diff --git a/apps/admin-x-activitypub/src/utils/get-username.ts b/apps/admin-x-activitypub/src/utils/get-username.ts deleted file mode 100644 index 2fd6099e337..00000000000 --- a/apps/admin-x-activitypub/src/utils/get-username.ts +++ /dev/null @@ -1,12 +0,0 @@ -function getUsername(actor: {preferredUsername: string; id: string|null;}) { - if (!actor.preferredUsername || !actor.id) { - return '@unknown@unknown'; - } - try { - return `@${actor.preferredUsername}@${(new URL(actor.id)).hostname}`; - } catch (err) { - return '@unknown@unknown'; - } -} - -export default getUsername; diff --git a/apps/admin-x-activitypub/test/acceptance/app.test.ts b/apps/admin-x-activitypub/test/acceptance/app.test.ts index 90fa60d8428..9e84a05428c 100644 --- a/apps/admin-x-activitypub/test/acceptance/app.test.ts +++ b/apps/admin-x-activitypub/test/acceptance/app.test.ts @@ -5,6 +5,6 @@ test.describe('Demo', async () => { test('Renders the list page', async ({page}) => { await page.goto('/'); - await expect(page.locator('body')).toContainText('ActivityPub Inbox'); + await expect(page.locator('body')).toContainText('ActivityPub Demo'); }); }); diff --git a/apps/admin-x-activitypub/test/acceptance/listIndex.test.ts b/apps/admin-x-activitypub/test/acceptance/listIndex.test.ts deleted file mode 100644 index 8e855d1e215..00000000000 --- a/apps/admin-x-activitypub/test/acceptance/listIndex.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import {expect, test} from '@playwright/test'; -import {mockApi, responseFixtures} from '@tryghost/admin-x-framework/test/acceptance'; - -test.describe('ListIndex', async () => { - test('Renders the list page', async ({page}) => { - const userId = 'index'; - await mockApi({ - page, - requests: { - useBrowseInboxForUser: {method: 'GET', path: `/inbox/${userId}`, response: responseFixtures.activitypubInbox}, - useBrowseFollowingForUser: {method: 'GET', path: `/following/${userId}`, response: responseFixtures.activitypubFollowing} - }, - options: {useActivityPub: true} - }); - - // Printing browser consol logs - page.on('console', (msg) => { - console.log(`Browser console log: ${msg.type()}: ${msg.text()}`); /* eslint-disable-line no-console */ - }); - - await page.goto('/'); - - await expect(page.locator('body')).toContainText('ActivityPub Inbox'); - - // following list - const followingUser = await page.locator('[data-test-following] > li').textContent(); - await expect(followingUser).toEqual('@index@main.ghost.org'); - const followingCount = await page.locator('[data-test-following-count]').textContent(); - await expect(followingCount).toEqual('1'); - - // following button - const followingList = await page.locator('[data-test-following-modal]'); - await expect(followingList).toBeVisible(); - - // activities - const activity = await page.locator('[data-test-activity-heading]').textContent(); - await expect(activity).toEqual('Testing ActivityPub'); - - // click on article - const articleBtn = await page.locator('[data-test-view-article]'); - await articleBtn.click(); - - // article is expanded - const frameLocator = page.frameLocator('#gh-ap-article-iframe'); - const textElement = await frameLocator.locator('[data-test-article-heading]').innerText(); - expect(textElement).toContain('Testing ActivityPub'); - - // go back to list - const backBtn = await page.locator('[data-test-back-button]'); - await backBtn.click(); - }); -}); diff --git a/apps/admin-x-activitypub/test/unit/ListIndex.test.tsx b/apps/admin-x-activitypub/test/unit/ListIndex.test.tsx new file mode 100644 index 00000000000..50459c8584e --- /dev/null +++ b/apps/admin-x-activitypub/test/unit/ListIndex.test.tsx @@ -0,0 +1,10 @@ +import ListIndex from '../../src/components/ListIndex'; +import {render, screen} from '@testing-library/react'; + +describe('Demo', function () { + it('renders a component', async function () { + render(); + + expect(screen.getAllByRole('heading')[0].textContent).toEqual('ActivityPub Demo'); + }); +}); diff --git a/apps/admin-x-activitypub/test/unit/utils/get-username.test.tsx b/apps/admin-x-activitypub/test/unit/utils/get-username.test.tsx deleted file mode 100644 index 8ca868b1c9b..00000000000 --- a/apps/admin-x-activitypub/test/unit/utils/get-username.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import getUsername from '../../../src/utils/get-username'; - -describe('getUsername', function () { - it('returns the formatted username', async function () { - const user = { - preferredUsername: 'index', - id: 'https://www.platformer.news/' - }; - - const result = getUsername(user); - - expect(result).toBe('@index@www.platformer.news'); - }); - - it('returns a default username if the user object is missing data', async function () { - const user = { - preferredUsername: '', - id: '' - }; - - const result = getUsername(user); - - expect(result).toBe('@unknown@unknown'); - }); - - it('returns a default username if url parsing fails', async function () { - const user = { - preferredUsername: 'index', - id: 'not-a-url' - }; - - const result = getUsername(user); - - expect(result).toBe('@unknown@unknown'); - }); -}); diff --git a/apps/admin-x-demo/package.json b/apps/admin-x-demo/package.json index 426e969f5c4..0c8088c869a 100644 --- a/apps/admin-x-demo/package.json +++ b/apps/admin-x-demo/package.json @@ -35,7 +35,7 @@ "@testing-library/react": "14.1.0", "@tryghost/admin-x-design-system": "0.0.0", "@tryghost/admin-x-framework": "0.0.0", - "@types/react": "18.3.3", + "@types/react": "18.3.2", "@types/react-dom": "18.3.0", "react": "18.3.1", "react-dom": "18.3.1" diff --git a/apps/admin-x-design-system/package.json b/apps/admin-x-design-system/package.json index 1b075723a93..e4e3cad1a0d 100644 --- a/apps/admin-x-design-system/package.json +++ b/apps/admin-x-design-system/package.json @@ -26,13 +26,13 @@ "tailwind.config.cjs" ], "devDependencies": { - "@codemirror/lang-html": "6.4.9", - "@storybook/addon-essentials": "7.6.20", - "@storybook/addon-interactions": "7.6.20", - "@storybook/addon-links": "7.6.20", + "@codemirror/lang-html": "^6.4.5", + "@storybook/addon-essentials": "7.6.19", + "@storybook/addon-interactions": "7.6.19", + "@storybook/addon-links": "7.6.19", "@storybook/addon-styling": "1.3.7", - "@storybook/blocks": "7.6.20", - "@storybook/react": "7.6.20", + "@storybook/blocks": "7.6.19", + "@storybook/react": "7.6.19", "@storybook/react-vite": "7.6.4", "@storybook/testing-library": "0.2.2", "@testing-library/react": "14.1.0", @@ -41,13 +41,12 @@ "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-refresh": "0.4.3", "eslint-plugin-tailwindcss": "3.13.0", - "jsdom": "24.1.0", "mocha": "10.2.0", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", "rollup-plugin-node-builtins": "2.1.2", "sinon": "17.0.0", - "storybook": "7.6.20", + "storybook": "7.6.19", "ts-node": "10.9.2", "typescript": "5.4.5", "vite": "4.5.3", @@ -57,18 +56,18 @@ "@dnd-kit/core": "6.1.0", "@dnd-kit/sortable": "7.0.2", "@ebay/nice-modal-react": "1.2.13", - "@sentry/react": "7.118.0", + "@sentry/react": "7.114.0", "@tailwindcss/forms": "0.5.7", "@tailwindcss/line-clamp": "0.4.4", - "@uiw/react-codemirror": "4.22.2", + "@uiw/react-codemirror": "^4.21.9", "autoprefixer": "10.4.19", "clsx": "2.1.1", - "postcss": "8.4.39", + "postcss": "8.4.38", "postcss-import": "16.1.0", - "react-colorful": "5.6.1", + "react-colorful": "^5.1.2", "react-hot-toast": "2.4.1", "react-select": "5.8.0", - "tailwindcss": "3.4.4" + "tailwindcss": "3.4.3" }, "peerDependencies": { "react": "^18.2.0", diff --git a/apps/admin-x-design-system/src/global/Button.tsx b/apps/admin-x-design-system/src/global/Button.tsx index 2464e98f15a..5c91bfeb26b 100644 --- a/apps/admin-x-design-system/src/global/Button.tsx +++ b/apps/admin-x-design-system/src/global/Button.tsx @@ -3,7 +3,7 @@ import React, {HTMLProps} from 'react'; import clsx from 'clsx'; import {LoadingIndicator, LoadingIndicatorColor, LoadingIndicatorSize} from './LoadingIndicator'; -export type ButtonColor = 'clear' | 'light-grey' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline'; +export type ButtonColor = 'clear' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline'; export type ButtonSize = 'sm' | 'md'; export interface ButtonProps extends Omit, 'label' | 'size' | 'children'> { @@ -75,13 +75,6 @@ const Button: React.FC = ({ loadingIndicatorColor = 'light'; iconColorClass = iconColorClass || 'text-white'; break; - case 'light-grey': - className = clsx( - link ? 'text-grey-800 hover:text-green-400 dark:text-white' : `bg-grey-200 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:!bg-grey-300 dark:hover:!bg-grey-800'}`, - className - ); - loadingIndicatorColor = 'dark'; - break; case 'grey': className = clsx( link ? 'text-black hover:text-grey-800 dark:text-white' : `bg-grey-100 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:!bg-grey-300 dark:hover:!bg-grey-800'}`, @@ -121,7 +114,7 @@ const Button: React.FC = ({ break; default: className = clsx( - link ? ' text-black hover:text-grey-800 dark:text-white' : `text-grey-900 dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200 hover:text-black'}`, + link ? ' text-black hover:text-grey-800 dark:text-white' : `text-black dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200'}`, (outlineOnMobile && !link) && 'border border-grey-300 hover:border-transparent md:border-transparent', className ); diff --git a/apps/admin-x-design-system/src/global/ButtonGroup.stories.tsx b/apps/admin-x-design-system/src/global/ButtonGroup.stories.tsx index 850c8d7baba..eb007ad9ca1 100644 --- a/apps/admin-x-design-system/src/global/ButtonGroup.stories.tsx +++ b/apps/admin-x-design-system/src/global/ButtonGroup.stories.tsx @@ -33,14 +33,6 @@ export const Default: Story = { } }; -export const Small: Story = { - args: { - buttons: defaultButtons, - link: false, - size: 'sm' - } -}; - const linkButtons: ButtonProps[] = [ { label: 'Cancel', @@ -58,4 +50,21 @@ export const LinkButtons: Story = { buttons: linkButtons, link: true } +}; + +export const WithBackground: Story = { + args: { + buttons: linkButtons, + link: true, + clearBg: false + } +}; + +export const SmallWithBackground: Story = { + args: { + buttons: linkButtons, + link: true, + clearBg: false, + size: 'sm' + } }; \ No newline at end of file diff --git a/apps/admin-x-design-system/src/global/ButtonGroup.tsx b/apps/admin-x-design-system/src/global/ButtonGroup.tsx index 26697d49776..f41d9b0fa4f 100644 --- a/apps/admin-x-design-system/src/global/ButtonGroup.tsx +++ b/apps/admin-x-design-system/src/global/ButtonGroup.tsx @@ -17,7 +17,7 @@ export interface ButtonGroupProps { const ButtonGroup: React.FC = ({size = 'md', buttons, link, linkWithPadding, clearBg = true, outlineOnMobile, className}) => { let groupColorClasses = clsx( 'flex items-center justify-start rounded', - link ? 'gap-4' : 'gap-2', + link ? 'gap-4' : 'gap-3', className ); @@ -33,7 +33,7 @@ const ButtonGroup: React.FC = ({size = 'md', buttons, link, li return (
    {buttons.map(({key, ...props}) => ( -
    ); diff --git a/apps/admin-x-design-system/src/global/Toast.stories.tsx b/apps/admin-x-design-system/src/global/Toast.stories.tsx index bdb5deb86ba..90cef8de10f 100644 --- a/apps/admin-x-design-system/src/global/Toast.stories.tsx +++ b/apps/admin-x-design-system/src/global/Toast.stories.tsx @@ -1,6 +1,7 @@ import type {Meta, StoryObj} from '@storybook/react'; import {ReactNode} from 'react'; +import {Toaster} from 'react-hot-toast'; import Button from './Button'; import {ShowToastProps, showToast} from './Toast'; @@ -24,6 +25,7 @@ const meta = { tags: ['autodocs'], decorators: [(_story: () => ReactNode) => ( <> + {_story()} )] diff --git a/apps/admin-x-design-system/src/global/form/TextArea.tsx b/apps/admin-x-design-system/src/global/form/TextArea.tsx index 5bda018ca5c..6a3c1e3b66c 100644 --- a/apps/admin-x-design-system/src/global/form/TextArea.tsx +++ b/apps/admin-x-design-system/src/global/form/TextArea.tsx @@ -18,6 +18,7 @@ export interface TextAreaProps extends HTMLProps { error?: boolean; placeholder?: string; hint?: React.ReactNode; + clearBg?: boolean; fontStyle?: FontStyles; className?: string; onChange?: (event: React.ChangeEvent) => void; diff --git a/apps/admin-x-design-system/src/global/form/Toggle.tsx b/apps/admin-x-design-system/src/global/form/Toggle.tsx index 4a3df9987f1..5a056cd21a7 100644 --- a/apps/admin-x-design-system/src/global/form/Toggle.tsx +++ b/apps/admin-x-design-system/src/global/form/Toggle.tsx @@ -9,7 +9,6 @@ export type ToggleDirections = 'ltr' | 'rtl'; export interface ToggleProps { checked?: boolean; disabled?: boolean; - name?: string; error?: boolean; size?: ToggleSizes; label?: React.ReactNode; @@ -34,7 +33,6 @@ const Toggle: React.FC = ({ error, checked, disabled, - name, onChange }) => { const id = useId(); @@ -97,7 +95,6 @@ const Toggle: React.FC = ({ )} disabled={disabled} id={id} - name={name} role="switch" type="checkbox" onChange={onChange} /> diff --git a/apps/admin-x-design-system/src/global/layout/ViewContainer.tsx b/apps/admin-x-design-system/src/global/layout/ViewContainer.tsx index 38289b4bad5..f7878b71db5 100644 --- a/apps/admin-x-design-system/src/global/layout/ViewContainer.tsx +++ b/apps/admin-x-design-system/src/global/layout/ViewContainer.tsx @@ -251,7 +251,7 @@ const ViewContainer: React.FC = ({ return (
    - {(title || actions || headerContent || tabs) && toolbar} + {(title || actions || headerContent) && toolbar}
    {mainContent}
    diff --git a/apps/admin-x-design-system/src/global/modal/Modal.tsx b/apps/admin-x-design-system/src/global/modal/Modal.tsx index e959a3c89f1..2377c3de3fb 100644 --- a/apps/admin-x-design-system/src/global/modal/Modal.tsx +++ b/apps/admin-x-design-system/src/global/modal/Modal.tsx @@ -27,7 +27,6 @@ export interface ModalProps { cancelLabel?: string; leftButtonProps?: ButtonProps; buttonsDisabled?: boolean; - okDisabled?: boolean; footer?: boolean | React.ReactNode; header?: boolean; padding?: boolean; @@ -63,7 +62,6 @@ const Modal: React.FC = ({ header, leftButtonProps, buttonsDisabled, - okDisabled, padding = true, onOk, okColor = 'black', @@ -181,7 +179,7 @@ const Modal: React.FC = ({ color: okColor, className: 'min-w-[80px]', onClick: onOk, - disabled: buttonsDisabled || okDisabled, + disabled: buttonsDisabled, loading: okLoading }); } diff --git a/apps/admin-x-design-system/src/settings/SettingGroup.tsx b/apps/admin-x-design-system/src/settings/SettingGroup.tsx index 2a620534fc2..7c3ab39191c 100644 --- a/apps/admin-x-design-system/src/settings/SettingGroup.tsx +++ b/apps/admin-x-design-system/src/settings/SettingGroup.tsx @@ -78,7 +78,6 @@ const SettingGroup = forwardRef(function Sett styles += ' border-grey-250 dark:border-grey-925'; - // The links visible before editing const viewButtons: ButtonProps[] = []; if (!hideEditButton) { @@ -90,7 +89,7 @@ const SettingGroup = forwardRef(function Sett { label, key: 'edit', - color: 'clear', + color: 'green', onClick: handleEdit } ); @@ -105,7 +104,6 @@ const SettingGroup = forwardRef(function Sett ); } - // The buttons that show when you are editing const editButtons: ButtonProps[] = [ { label: 'Cancel', @@ -121,10 +119,9 @@ const SettingGroup = forwardRef(function Sett } editButtons.push( { - label: label, + label, key: 'save', - color: saveState === 'unsaved' ? 'green' : 'light-grey', - disabled: saveState !== 'unsaved', + color: 'green', onClick: handleSave } ); @@ -154,35 +151,18 @@ const SettingGroup = forwardRef(function Sett styles ); - if (!isEditing) { - return ( -
    -
    - {customHeader ? customHeader : - - {customButtons ? customButtons : - (onEditingChange && ) - } - - } - {children} -
    - ); - } else { - return ( -
    -
    - {customHeader ? customHeader : - - {customButtons ? customButtons : - (onEditingChange && ) - } - - } - {children} -
    - ); - } + return ( +
    +
    + {customHeader ? customHeader : + + {customButtons ? customButtons : + (onEditingChange && )} + + } + {children} +
    + ); }); export default SettingGroup; diff --git a/apps/admin-x-design-system/src/settings/SettingGroupHeader.tsx b/apps/admin-x-design-system/src/settings/SettingGroupHeader.tsx index 89278245438..181196bcf37 100644 --- a/apps/admin-x-design-system/src/settings/SettingGroupHeader.tsx +++ b/apps/admin-x-design-system/src/settings/SettingGroupHeader.tsx @@ -14,10 +14,10 @@ const SettingGroupHeader: React.FC = ({title, descripti {(title || description) &&
    {title}{beta && Beta} - {description &&

    {description}

    } + {description &&

    {description}

    }
    } -
    +
    {children}
    diff --git a/apps/admin-x-design-system/tailwind.config.cjs b/apps/admin-x-design-system/tailwind.config.cjs index 7a614ce4f67..d554015aa49 100644 --- a/apps/admin-x-design-system/tailwind.config.cjs +++ b/apps/admin-x-design-system/tailwind.config.cjs @@ -270,7 +270,7 @@ module.exports = { base: '1.4rem', xs: '1.2rem', sm: '1.3rem', - md: '1.4rem', + md: '1.40rem', lg: '1.65rem', xl: '2rem', '2xl': '2.4rem', diff --git a/apps/admin-x-framework/package.json b/apps/admin-x-framework/package.json index 5381205abd2..ce52902a2ca 100644 --- a/apps/admin-x-framework/package.json +++ b/apps/admin-x-framework/package.json @@ -73,7 +73,6 @@ "c8": "8.0.1", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-refresh": "0.4.3", - "jsdom": "24.1.0", "mocha": "10.2.0", "react": "18.3.1", "react-dom": "18.3.1", @@ -82,14 +81,14 @@ "typescript": "5.4.5" }, "dependencies": { - "@sentry/react": "7.118.0", + "@sentry/react": "7.114.0", "@tanstack/react-query": "4.36.1", "@tryghost/admin-x-design-system": "0.0.0", - "@types/react": "18.3.3", + "@types/react": "18.3.2", "@types/react-dom": "18.3.0", "@vitejs/plugin-react": "4.2.1", - "react": "18.3.1", - "react-dom": "18.3.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", "vite": "4.5.3", "vite-plugin-css-injected-by-js": "^3.3.0", "vite-plugin-svgr": "3.3.0", diff --git a/apps/admin-x-framework/src/api/actions.ts b/apps/admin-x-framework/src/api/actions.ts index 43a00db4a59..e9f015bcde4 100644 --- a/apps/admin-x-framework/src/api/actions.ts +++ b/apps/admin-x-framework/src/api/actions.ts @@ -78,7 +78,7 @@ export const useBrowseActions = createInfiniteQuery({ } }); - const meta = pages[pages.length - 1].meta; + const meta = pages.at(-1)!.meta; return { actions: actions.reverse(), diff --git a/apps/admin-x-framework/src/api/activitypub.ts b/apps/admin-x-framework/src/api/activitypub.ts deleted file mode 100644 index 246292d5560..00000000000 --- a/apps/admin-x-framework/src/api/activitypub.ts +++ /dev/null @@ -1,122 +0,0 @@ -import {createMutation, createQueryWithId} from '../utils/api/hooks'; - -export type FollowItem = { - id: string; - preferredUsername: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [x: string]: any -}; - -export type ObjectProperties = { - '@context': string | (string | object)[]; - type: 'Article' | 'Link'; - name: string; - content: string; - url?: string | undefined; - attributedTo?: string | object[] | undefined; - image?: string; - published?: string; - preview?: {type: string, content: string}; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [x: string]: any; -} - -export type ActorProperties = { - '@context': string | (string | object)[]; - attachment: object[]; - discoverable: boolean; - featured: string; - followers: string; - following: string; - id: string | null; - image: string; - inbox: string; - manuallyApprovesFollowers: boolean; - name: string; - outbox: string; - preferredUsername: string; - publicKey: { - id: string; - owner: string; - publicKeyPem: string; - }; - published: string; - summary: string; - type: 'Person'; - url: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [x: string]: any; -} - -export type Activity = { - '@context': string; - id: string; - type: string; - actor: ActorProperties; - object: ObjectProperties; - to: string; -} - -export type InboxResponseData = { - '@context': string; - id: string; - summary: string; - type: 'OrderedCollection'; - totalItems: number; - items: Activity[]; -} - -export type FollowingResponseData = { - '@context': string; - id: string; - summary: string; - type: string; - totalItems: number; - items: FollowItem[]; -} - -type FollowRequestProps = { - username: string -} - -export const useFollow = createMutation({ - method: 'POST', - useActivityPub: true, - path: data => `/actions/follow/${data.username}` -}); - -export const useUnfollow = createMutation({ - method: 'POST', - useActivityPub: true, - path: data => `/actions/unfollow/${data.username}` -}); - -// This is a frontend root, not using the Ghost admin API -export const useBrowseInboxForUser = createQueryWithId({ - dataType: 'InboxResponseData', - useActivityPub: true, - headers: { - Accept: 'application/activity+json' - }, - path: id => `/inbox/${id}` -}); - -// This is a frontend root, not using the Ghost admin API -export const useBrowseFollowingForUser = createQueryWithId({ - dataType: 'FollowingResponseData', - useActivityPub: true, - headers: { - Accept: 'application/activity+json' - }, - path: id => `/following/${id}` -}); - -// This is a frontend root, not using the Ghost admin API -export const useBrowseFollowersForUser = createQueryWithId({ - dataType: 'FollowingResponseData', - useActivityPub: true, - headers: { - Accept: 'application/activity+json' - }, - path: id => `/followers/${id}` -}); diff --git a/apps/admin-x-framework/src/api/newsletters.ts b/apps/admin-x-framework/src/api/newsletters.ts index b74f8cbbbba..481a07dfd42 100644 --- a/apps/admin-x-framework/src/api/newsletters.ts +++ b/apps/admin-x-framework/src/api/newsletters.ts @@ -21,7 +21,6 @@ export type Newsletter = { show_header_title: boolean; title_font_category: string; title_alignment: string; - show_excerpt: boolean; show_feature_image: boolean; body_font_category: string; footer_content: string | null; @@ -61,7 +60,7 @@ export const useBrowseNewsletters = createInfiniteQuery { const {pages} = originalData as InfiniteData; const newsletters = pages.flatMap(page => page.newsletters); - const meta = pages[pages.length - 1].meta; + const meta = pages.at(-1)!.meta; return { newsletters: newsletters, diff --git a/apps/admin-x-framework/src/api/tiers.ts b/apps/admin-x-framework/src/api/tiers.ts index 1813846cafc..6ac9b67566c 100644 --- a/apps/admin-x-framework/src/api/tiers.ts +++ b/apps/admin-x-framework/src/api/tiers.ts @@ -41,7 +41,7 @@ export const useBrowseTiers = createInfiniteQuery { const {pages} = originalData as InfiniteData; const tiers = pages.flatMap(page => page.tiers); - const meta = pages[pages.length - 1].meta; + const meta = pages.at(-1)!.meta; return { tiers, diff --git a/apps/admin-x-framework/src/api/users.ts b/apps/admin-x-framework/src/api/users.ts index b5c8b855a40..9df81ed45f6 100644 --- a/apps/admin-x-framework/src/api/users.ts +++ b/apps/admin-x-framework/src/api/users.ts @@ -76,7 +76,7 @@ export const useBrowseUsers = createInfiniteQuery { const {pages} = originalData as InfiniteData; const users = pages.flatMap(page => page.users); - const meta = pages[pages.length - 1].meta; + const meta = pages.at(-1)!.meta; return { users: users, diff --git a/apps/admin-x-framework/src/hooks/useFilterableApi.ts b/apps/admin-x-framework/src/hooks/useFilterableApi.ts index 61b568916a7..6b738e8cd99 100644 --- a/apps/admin-x-framework/src/hooks/useFilterableApi.ts +++ b/apps/admin-x-framework/src/hooks/useFilterableApi.ts @@ -7,7 +7,7 @@ const escapeNqlString = (value: string) => { }; const useFilterableApi = < - Data extends {id: string} & {[k in FilterKey]: string} & {[k: string]: unknown}, + Data extends {id: string} & {[Key in FilterKey]: string}, ResponseKey extends string = string, FilterKey extends string = string >({path, filterKey, responseKey, limit = 20}: { @@ -41,27 +41,26 @@ const useFilterableApi = < return response[responseKey]; }; - const loadInitialValues = async (values: string[], key: string) => { - await loadData(''); + return { + loadData, - const data = [...(result.current.data || [])]; - const missingValues = values.filter(value => !result.current.data?.find(item => item[key] === value)); + loadInitialValues: async (ids: string[]) => { + await loadData(''); - if (missingValues.length) { - const additionalData = await fetchApi<{meta?: Meta} & {[k in ResponseKey]: Data[]}>(apiUrl(path, { - filter: `${key}:[${missingValues.join(',')}]`, - limit: 'all' - })); + const data = [...(result.current.data || [])]; + const missingIds = ids.filter(id => !result.current.data?.find(({id: dataId}) => dataId === id)); - data.push(...additionalData[responseKey]); - } + if (missingIds.length) { + const additionalData = await fetchApi<{meta?: Meta} & {[k in ResponseKey]: Data[]}>(apiUrl(path, { + filter: `id:[${missingIds.join(',')}]`, + limit: 'all' + })); - return values.map(value => data.find(item => item[key] === value)!); - }; + data.push(...additionalData[responseKey]); + } - return { - loadData, - loadInitialValues + return ids.map(id => data.find(({id: dataId}) => dataId === id)!); + } }; }; diff --git a/apps/admin-x-framework/src/test/acceptance.ts b/apps/admin-x-framework/src/test/acceptance.ts index b22e4ac92a6..6bebb95f4e2 100644 --- a/apps/admin-x-framework/src/test/acceptance.ts +++ b/apps/admin-x-framework/src/test/acceptance.ts @@ -16,8 +16,6 @@ import siteFixture from './responses/site.json'; import themesFixture from './responses/themes.json'; import tiersFixture from './responses/tiers.json'; import usersFixture from './responses/users.json'; -import activitypubInboxFixture from './responses/activitypub/inbox.json'; -import activitypubFollowingFixture from './responses/activitypub/following.json'; import {ActionsResponseType} from '../api/actions'; import {ConfigResponseType} from '../api/config'; @@ -65,9 +63,7 @@ export const responseFixtures = { themes: themesFixture as ThemesResponseType, newsletters: newslettersFixture as NewslettersResponseType, actions: actionsFixture as ActionsResponseType, - latestPost: {posts: [{id: '1', url: `${siteFixture.site.url}/test-post/`}]}, - activitypubInbox: activitypubInboxFixture, - activitypubFollowing: activitypubFollowingFixture + latestPost: {posts: [{id: '1', url: `${siteFixture.site.url}/test-post/`}]} }; const defaultLabFlags = { @@ -149,7 +145,7 @@ export const limitRequests = { browseNewslettersLimit: {method: 'GET', path: '/newsletters/?filter=status%3Aactive&limit=1', response: responseFixtures.newsletters} }; -export async function mockApi>({page, requests, options = {}}: {page: Page, requests: Requests, options?: {useActivityPub?: boolean}}) { +export async function mockApi>({page, requests}: {page: Page, requests: Requests}) { const lastApiRequests: {[key in keyof Requests]?: RequestRecord} = {}; const namedRequests = Object.entries(requests).reduce( @@ -157,11 +153,8 @@ export async function mockApi [] as Array ); - const routeRegex = options?.useActivityPub ? /\/activitypub\// : /\/ghost\/api\/admin\//; - const routeReplaceRegex = options.useActivityPub ? /^.*\/activitypub/ : /^.*\/ghost\/api\/admin/; - - await page.route(routeRegex, async (route) => { - const apiPath = route.request().url().replace(routeReplaceRegex, ''); + await page.route(/\/ghost\/api\/admin\//, async (route) => { + const apiPath = route.request().url().replace(/^.*\/ghost\/api\/admin/, ''); const matchingMock = namedRequests.find((request) => { if (request.method !== route.request().method()) { diff --git a/apps/admin-x-framework/src/test/responses/activitypub/following.json b/apps/admin-x-framework/src/test/responses/activitypub/following.json deleted file mode 100644 index f374bedb4e3..00000000000 --- a/apps/admin-x-framework/src/test/responses/activitypub/following.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://0a2e-129-222-88-174.ngrok-free.app/activitypub/following/deadbeefdeadbeefdeadbeef", - "summary": "Following collection for index", - "type": "Collection", - "totalItems": 1, - "items": [ - { - "id": "https://main.ghost.org/activitypub/actor/deadbeefdeadbeefdeadbeef", - "username": "@index@main.ghost.org" - } - ] - } diff --git a/apps/admin-x-framework/src/test/responses/activitypub/inbox.json b/apps/admin-x-framework/src/test/responses/activitypub/inbox.json deleted file mode 100644 index 550fda9489f..00000000000 --- a/apps/admin-x-framework/src/test/responses/activitypub/inbox.json +++ /dev/null @@ -1,155 +0,0 @@ -{ - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.com/activitypub/inbox/index", - "summary": "Inbox for index", - "type": "OrderedCollection", - "totalItems": 2, - "orderedItems": [ - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://main.ghost.org/activitypub/activity/664cf007fd27b20001a76d72", - "type": "Accept", - "actor": { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - { - "featured": { - "@id": "http://joinmastodon.org/ns#featured", - "@type": "@id" - } - }, - { - "discoverable": { - "@id": "http://joinmastodon.org/ns#discoverable", - "@type": "@id" - } - }, - { - "manuallyApprovesFollowers": { - "@id": "http://joinmastodon.org/ns#manuallyApprovesFollowers", - "@type": "@id" - } - }, - { - "schema": "http://schema.org#", - "PropertyValue": "schema:PropertyValue", - "value": "schema:value" - } - ], - "type": "Person", - "id": "https://main.ghost.org/activitypub/actor/index", - "name": "The Main", - "preferredUsername": "index", - "summary": "The bio for the actor", - "url": "https://main.ghost.org/activitypub/actor/index", - "icon": "", - "image": "", - "published": "1970-01-01T00:00:00Z", - "manuallyApprovesFollowers": false, - "discoverable": true, - "attachment": [ - { - "type": "PropertyValue", - "name": "Website", - "value": "main.ghost.org" - } - ], - "following": "https://main.ghost.org/activitypub/following/index", - "followers": "https://main.ghost.org/activitypub/followers/index", - "inbox": "https://main.ghost.org/activitypub/inbox/index", - "outbox": "https://main.ghost.org/activitypub/outbox/index", - "featured": "https://main.ghost.org/activitypub/featured/index", - "publicKey": { - "id": "https://main.ghost.org/activitypub/actor/index#main-key", - "owner": "https://main.ghost.org/activitypub/actor/index", - "publicKeyPem": "-----BEGIN RSA PUBLIC KEY-----\nMIGJAoGBANRpUrwk7x7bJDddHmrYSWVw9enVPMFm5qAW7fTgoZ7x2PoJUIqy/bkqpXZ0SmZs\nsLO3UZm+yN/DqxioD8BnhhD0N8Ydv6+UniT7hE2tHvsMxQIq2jet1auSBZNFmUIWodsBxI/R\ntm+KwFBFk+P+MvVsGZ2K3Rkd4K0dv0/45dtXAgMBAAE=\n-----END RSA PUBLIC KEY-----\n" - } - }, - "object": { - "id": "https://0a2e-129-222-88-174.ngrok-free.app/activitypub/activity/664cf0074daa2f8183ba6ea6", - "type": "Follow" - }, - "to": "https://0a2e-129-222-88-174.ngrok-free.app/activitypub/actor/index" - }, - { - "type": "Create", - "actor": { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - { - "featured": { - "@id": "http://joinmastodon.org/ns#featured", - "@type": "@id" - } - }, - { - "discoverable": { - "@id": "http://joinmastodon.org/ns#discoverable", - "@type": "@id" - } - }, - { - "manuallyApprovesFollowers": { - "@id": "http://joinmastodon.org/ns#manuallyApprovesFollowers", - "@type": "@id" - } - }, - { - "schema": "http://schema.org#", - "PropertyValue": "schema:PropertyValue", - "value": "schema:value" - } - ], - "type": "Person", - "id": "https://main.ghost.org/activitypub/actor/index", - "name": "The Main", - "preferredUsername": "index", - "summary": "The bio for the actor", - "url": "https://main.ghost.org/activitypub/actor/index", - "icon": "", - "image": "", - "published": "1970-01-01T00:00:00Z", - "manuallyApprovesFollowers": false, - "discoverable": true, - "attachment": [ - { - "type": "PropertyValue", - "name": "Website", - "value": "main.ghost.org" - } - ], - "following": "https://main.ghost.org/activitypub/following/index", - "followers": "https://main.ghost.org/activitypub/followers/index", - "inbox": "https://main.ghost.org/activitypub/inbox/index", - "outbox": "https://main.ghost.org/activitypub/outbox/index", - "featured": "https://main.ghost.org/activitypub/featured/index", - "publicKey": { - "id": "https://main.ghost.org/activitypub/actor/index#main-key", - "owner": "https://main.ghost.org/activitypub/actor/index", - "publicKeyPem": "-----BEGIN RSA PUBLIC KEY-----\nMIGJAoGBANRpUrwk7x7bJDddHmrYSWVw9enVPMFm5qAW7fTgoZ7x2PoJUIqy/bkqpXZ0SmZs\nsLO3UZm+yN/DqxioD8BnhhD0N8Ydv6+UniT7hE2tHvsMxQIq2jet1auSBZNFmUIWodsBxI/R\ntm+KwFBFk+P+MvVsGZ2K3Rkd4K0dv0/45dtXAgMBAAE=\n-----END RSA PUBLIC KEY-----\n" - } - }, - "object": { - "@context": "https://www.w3.org/ns/activitystreams", - "type": "Article", - "id": "https://main.ghost.org/activitypub/article/my-article/", - "name": "Testing ActivityPub", - "content": "

    Super long test

    ", - "url": "https://main.ghost.org/my-article/", - "image": "https://main.ghost.org/content/images/2021/08/ghost-logo.png", - "published": "2024-05-09T00:00:00Z", - "attributedTo": { - "type": "Person", - "name": "The Main" - }, - "preview": { - "type": "Link", - "href": "https://main.ghost.org/my-article/", - "name": "Testing ActivityPub" - } - } - } - ] -} diff --git a/apps/admin-x-framework/src/test/responses/newsletters.json b/apps/admin-x-framework/src/test/responses/newsletters.json index 7c94e4ccea8..96a971542e5 100644 --- a/apps/admin-x-framework/src/test/responses/newsletters.json +++ b/apps/admin-x-framework/src/test/responses/newsletters.json @@ -19,7 +19,6 @@ "show_header_title": true, "title_font_category": "serif", "title_alignment": "center", - "show_excerpt": true, "show_feature_image": true, "body_font_category": "serif", "footer_content": "", diff --git a/apps/admin-x-framework/src/utils/api/fetchApi.ts b/apps/admin-x-framework/src/utils/api/fetchApi.ts index 352fd2f3db2..09671853402 100644 --- a/apps/admin-x-framework/src/utils/api/fetchApi.ts +++ b/apps/admin-x-framework/src/utils/api/fetchApi.ts @@ -74,6 +74,10 @@ export const useFetchApi = () => { ...options }); + if (attempts !== 0 && sentryDSN) { + Sentry.captureMessage('Request took multiple attempts', {extra: getErrorData()}); + } + return handleResponse(response) as ResponseData; } catch (error) { retryingMs = Date.now() - startTime; @@ -111,11 +115,10 @@ export const useFetchApi = () => { }; }; -const {apiRoot, activityPubRoot} = getGhostPaths(); +const {apiRoot} = getGhostPaths(); -export const apiUrl = (path: string, searchParams: Record = {}, useActivityPub: boolean = false) => { - const root = useActivityPub ? activityPubRoot : apiRoot; - const url = new URL(`${root}${path}`, window.location.origin); +export const apiUrl = (path: string, searchParams: Record = {}) => { + const url = new URL(`${apiRoot}${path}`, window.location.origin); url.search = new URLSearchParams(searchParams).toString(); return url.toString(); }; diff --git a/apps/admin-x-framework/src/utils/api/hooks.ts b/apps/admin-x-framework/src/utils/api/hooks.ts index d2818cdb9cd..23bb0a78a84 100644 --- a/apps/admin-x-framework/src/utils/api/hooks.ts +++ b/apps/admin-x-framework/src/utils/api/hooks.ts @@ -20,11 +20,9 @@ export interface Meta { interface QueryOptions { dataType: string path: string - headers?: Record; defaultSearchParams?: Record; permissions?: string[]; returnData?: (originalData: unknown) => ResponseData; - useActivityPub?: boolean; } type QueryHookOptions = UseQueryOptions & { @@ -33,14 +31,14 @@ type QueryHookOptions = UseQueryOptions & { }; export const createQuery = (options: QueryOptions) => ({searchParams, ...query}: QueryHookOptions = {}): Omit, 'data'> & {data: ResponseData | undefined} => { - const url = apiUrl(options.path, searchParams || options.defaultSearchParams, options?.useActivityPub); + const url = apiUrl(options.path, searchParams || options.defaultSearchParams); const fetchApi = useFetchApi(); const handleError = useHandleError(); const result = useQuery({ enabled: options.permissions ? usePermission(options.permissions) : true, queryKey: [options.dataType, url], - queryFn: () => fetchApi(url, {...options}), + queryFn: () => fetchApi(url), ...query }); @@ -67,7 +65,7 @@ export const createPaginatedQuery = (options const paginatedSearchParams = searchParams || options.defaultSearchParams || {}; paginatedSearchParams.page = page.toString(); - const url = apiUrl(options.path, paginatedSearchParams, options?.useActivityPub); + const url = apiUrl(options.path, paginatedSearchParams); const fetchApi = useFetchApi(); const handleError = useHandleError(); @@ -120,8 +118,8 @@ export const createInfiniteQuery = (options: InfiniteQueryOptions< const nextPageParams = getNextPageParams || options.defaultNextPageParams || (() => ({})); const result = useInfiniteQuery({ - queryKey: [options.dataType, apiUrl(options.path, searchParams || options.defaultSearchParams, options?.useActivityPub)], - queryFn: ({pageParam}) => fetchApi(apiUrl(options.path, pageParam || searchParams || options.defaultSearchParams, options?.useActivityPub)), + queryKey: [options.dataType, apiUrl(options.path, searchParams || options.defaultSearchParams)], + queryFn: ({pageParam}) => fetchApi(apiUrl(options.path, pageParam || searchParams || options.defaultSearchParams)), getNextPageParam: data => nextPageParams(data, searchParams || options.defaultSearchParams || {}), ...query }); @@ -147,7 +145,6 @@ export const createQueryWithId = (options: Omit extends Omit, 'dataType' | 'path'>, Omit { path: (payload: Payload) => string; - headers?: Record; body?: (payload: Payload) => FormData | object; searchParams?: (payload: Payload) => { [key: string]: string; }; invalidateQueries?: { dataType: string; }; @@ -162,7 +159,7 @@ const mutate = ({fetchApi, path, payload, searchParams, o options: Omit, 'path'> }) => { const {defaultSearchParams, body, ...requestOptions} = options; - const url = apiUrl(path, searchParams || defaultSearchParams, options?.useActivityPub); + const url = apiUrl(path, searchParams || defaultSearchParams); const generatedBody = payload && body?.(payload); let requestBody: string | FormData | undefined = undefined; diff --git a/apps/admin-x-framework/src/utils/api/updateQueries.ts b/apps/admin-x-framework/src/utils/api/updateQueries.ts index a797d2fd83b..bc2890c7235 100644 --- a/apps/admin-x-framework/src/utils/api/updateQueries.ts +++ b/apps/admin-x-framework/src/utils/api/updateQueries.ts @@ -10,7 +10,7 @@ export const insertToQueryCache = (field: string, recordsToInsert? if (typeof currentData === 'object' && 'pages' in currentData) { const {pages} = currentData as InfiniteData; - const lastPage = pages[pages.length - 1]; + const lastPage = pages.at(-1)!; return { ...currentData, pages: pages.slice(0, -1).concat({ diff --git a/apps/admin-x-framework/src/utils/errors.ts b/apps/admin-x-framework/src/utils/errors.ts index b8156d65d0a..bfbafc71a26 100644 --- a/apps/admin-x-framework/src/utils/errors.ts +++ b/apps/admin-x-framework/src/utils/errors.ts @@ -22,10 +22,10 @@ export class APIError extends Error { errorOptions?: ErrorOptions ) { if (!message && response && response.url.includes('/ghost/api/admin/')) { - message = `Something went wrong while loading ${response.url.replace(/.+\/ghost\/api\/admin\//, '').replace(/\W.*/, '').replace('_', ' ')}, please try again.`; + message = `${response.statusText}, cannot fetch ${response.url.replace(/.+\/ghost\/api\/admin\//, '').replace(/\W.*/, '').replace('_', ' ')}`; } - super(message || 'Something went wrong, please try again.', errorOptions); + super(message || 'Unknown error', errorOptions); } } @@ -48,13 +48,13 @@ export class VersionMismatchError extends JSONError { export class ServerUnreachableError extends APIError { constructor(errorOptions?: ErrorOptions) { - super(undefined, undefined, 'Something went wrong, please try again.', errorOptions); + super(undefined, undefined, 'Server was unreachable', errorOptions); } } export class TimeoutError extends APIError { constructor(errorOptions?: ErrorOptions) { - super(undefined, undefined, 'Request timed out, please try again.', errorOptions); + super(undefined, undefined, 'Request timed out', errorOptions); } } diff --git a/apps/admin-x-framework/src/utils/helpers.ts b/apps/admin-x-framework/src/utils/helpers.ts index 1207ff12c61..d05d5a4ef47 100644 --- a/apps/admin-x-framework/src/utils/helpers.ts +++ b/apps/admin-x-framework/src/utils/helpers.ts @@ -3,7 +3,6 @@ export interface IGhostPaths { adminRoot: string; assetRoot: string; apiRoot: string; - activityPubRoot: string; } export function getGhostPaths(): IGhostPaths { @@ -12,8 +11,7 @@ export function getGhostPaths(): IGhostPaths { const adminRoot = `${subdir}/ghost/`; const assetRoot = `${subdir}/ghost/assets/`; const apiRoot = `${subdir}/ghost/api/admin`; - const activityPubRoot = `${subdir}/.ghost/activitypub`; - return {subdir, adminRoot, assetRoot, apiRoot, activityPubRoot}; + return {subdir, adminRoot, assetRoot, apiRoot}; } export function downloadFile(url: string) { diff --git a/apps/admin-x-framework/src/utils/queryClient.ts b/apps/admin-x-framework/src/utils/queryClient.ts index 26a9cfe719c..7a3197e9166 100644 --- a/apps/admin-x-framework/src/utils/queryClient.ts +++ b/apps/admin-x-framework/src/utils/queryClient.ts @@ -13,8 +13,7 @@ const queryClient = window.adminXQueryClient || new QueryClient({ staleTime: 5 * (60 * 1000), // 5 mins cacheTime: 10 * (60 * 1000), // 10 mins // We have custom retry logic for specific errors in fetchApi() - retry: false, - networkMode: 'always' + retry: false } } }); diff --git a/apps/admin-x-framework/test/unit/utils/api/hooks.test.tsx b/apps/admin-x-framework/test/unit/utils/api/hooks.test.tsx index 2099441d963..1245c3c63f9 100644 --- a/apps/admin-x-framework/test/unit/utils/api/hooks.test.tsx +++ b/apps/admin-x-framework/test/unit/utils/api/hooks.test.tsx @@ -60,47 +60,12 @@ describe('API hooks', function () { expect(mock.calls.length).toBe(1); expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/', { credentials: 'include', - dataType: 'test', - headers: { - 'app-pragma': 'no-cache', - 'x-ghost-version': '5.x' - }, - method: 'GET', - mode: 'cors', - path: '/test/', - signal: expect.any(AbortSignal) - }]); - }); - }); - - it('can add custom headers', async function () { - await withMockFetch({ - json: {test: 1} - }, async (mock) => { - const useTestQuery = createQuery({ - dataType: 'test', - path: '/test/', - headers: {'Content-Type': 'ALOHA'} - }); - - const {result} = renderHook(() => useTestQuery(), {wrapper}); - - await waitFor(() => expect(result.current.isLoading).toBe(false)); - - expect(result.current.data).toEqual({test: 1}); - - expect(mock.calls.length).toBe(1); - expect(mock.calls[0]).toEqual(['http://localhost:3000/ghost/api/admin/test/', { - credentials: 'include', - dataType: 'test', headers: { - 'Content-Type': 'ALOHA', 'app-pragma': 'no-cache', 'x-ghost-version': '5.x' }, method: 'GET', mode: 'cors', - path: '/test/', signal: expect.any(AbortSignal) }]); }); diff --git a/apps/admin-x-settings/package.json b/apps/admin-x-settings/package.json index 63f09ab344c..c6d267311cb 100644 --- a/apps/admin-x-settings/package.json +++ b/apps/admin-x-settings/package.json @@ -37,10 +37,10 @@ "preview": "vite preview" }, "dependencies": { - "@codemirror/lang-html": "6.4.9", + "@codemirror/lang-html": "^6.4.5", "@tryghost/color-utils": "0.2.2", - "@tryghost/kg-unsplash-selector": "0.2.1", - "@tryghost/limit-service": "1.2.14", + "@tryghost/kg-unsplash-selector": "^0.1.15", + "@tryghost/limit-service": "^1.2.10", "@tryghost/nql": "0.12.3", "@tryghost/timezone-data": "0.4.3", "react": "18.3.1", @@ -52,9 +52,9 @@ "@testing-library/react": "14.1.0", "@tryghost/admin-x-design-system": "0.0.0", "@tryghost/admin-x-framework": "0.0.0", - "@types/react": "18.3.3", + "@types/react": "18.3.2", "@types/react-dom": "18.3.0", - "@types/validator": "13.12.0", + "@types/validator": "13.11.10", "@vitejs/plugin-react": "4.2.1", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-refresh": "0.4.3", diff --git a/apps/admin-x-settings/src/MainContent.tsx b/apps/admin-x-settings/src/MainContent.tsx index 31898d9b565..45bafb385a6 100644 --- a/apps/admin-x-settings/src/MainContent.tsx +++ b/apps/admin-x-settings/src/MainContent.tsx @@ -61,7 +61,7 @@ const MainContent: React.FC = () => { if (isEditorUser(currentUser)) { return ( -
    +
    Settings
    diff --git a/apps/admin-x-settings/src/components/settings/advanced/CodeInjection.tsx b/apps/admin-x-settings/src/components/settings/advanced/CodeInjection.tsx index 484d52319dd..aba04ba3c08 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/CodeInjection.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/CodeInjection.tsx @@ -10,7 +10,7 @@ const CodeInjection: React.FC<{ keywords: string[] }> = ({keywords}) => { customHeader={
    -
    diff --git a/apps/admin-x-settings/src/components/settings/advanced/History.tsx b/apps/admin-x-settings/src/components/settings/advanced/History.tsx index 8b05a073cc2..7f33ae13546 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/History.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/History.tsx @@ -11,7 +11,7 @@ const History: React.FC<{ keywords: string[] }> = ({keywords}) => { return ( } + customButtons={
    } ]; diff --git a/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterPreview.tsx b/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterPreview.tsx index f5679047243..7e94bbea2c7 100644 --- a/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterPreview.tsx +++ b/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterPreview.tsx @@ -106,7 +106,6 @@ const NewsletterPreview: React.FC<{newsletter: Newsletter}> = ({newsletter}) => senderReplyTo={renderReplyToEmail(newsletter, config, supportEmailAddress, defaultEmailAddress)} showBadge={newsletter.show_badge} showCommentCta={showCommentCta} - showExcerpt={newsletter.show_excerpt} showFeatureImage={newsletter.show_feature_image} showFeedback={showFeedback} showLatestPosts={newsletter.show_latest_posts} diff --git a/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterPreviewContent.tsx b/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterPreviewContent.tsx index e5f4642296c..0bffd1434ac 100644 --- a/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterPreviewContent.tsx +++ b/apps/admin-x-settings/src/components/settings/email/newsletters/NewsletterPreviewContent.tsx @@ -18,7 +18,6 @@ const NewsletterPreviewContent: React.FC<{ headerTitle?: string | null; headerSubtitle?: string | null; showPostTitleSection: boolean; - showExcerpt: boolean; titleAlignment?: string; titleFontCategory?: string; bodyFontCategory?: string; @@ -50,7 +49,6 @@ const NewsletterPreviewContent: React.FC<{ headerTitle, headerSubtitle, showPostTitleSection, - showExcerpt, titleAlignment, titleFontCategory, bodyFontCategory, @@ -77,7 +75,6 @@ const NewsletterPreviewContent: React.FC<{ const showHeader = headerIcon || headerTitle; const {config} = useGlobalData(); const hasNewEmailAddresses = useFeatureFlag('newEmailAddresses'); - const hasNewsletterExcerpt = useFeatureFlag('newsletterExcerpt'); const currentDate = new Date().toLocaleDateString('default', { year: 'numeric', @@ -101,25 +98,6 @@ const NewsletterPreviewContent: React.FC<{

    To: Jamie Larson jamie@example.com

    ; } - let excerptClasses = 'mb-5 text-pretty leading-[1.7] text-black'; - - if (titleFontCategory === 'serif' && bodyFontCategory === 'serif') { - excerptClasses = clsx(excerptClasses, 'mb-8 font-serif text-[2.0rem] leading-tight'); - } else if (titleFontCategory !== 'serif' && bodyFontCategory === 'serif') { - excerptClasses = clsx(excerptClasses, 'mb-8 text-[1.7rem] leading-tight tracking-tight'); - } else if (titleFontCategory === 'serif' && bodyFontCategory !== 'serif') { - excerptClasses = clsx(excerptClasses, 'mb-8 font-serif text-[2.0rem] leading-tight'); - } else { - excerptClasses = clsx(excerptClasses, 'mb-8 text-[1.9rem] leading-tight tracking-tight'); - } - - if (titleAlignment === 'center') { - excerptClasses = clsx( - excerptClasses, - 'text-center' - ); - } - return (
    @@ -134,38 +112,34 @@ const NewsletterPreviewContent: React.FC<{
    {headerImage && (
    - +
    )} {showHeader && ( -
    +
    {headerIcon && } {headerTitle &&

    {headerTitle}

    } - {headerSubtitle &&
    {headerSubtitle}
    } + {headerSubtitle &&
    {headerSubtitle}
    }
    )} {showPostTitleSection && ( -
    +

    Your email newsletter

    - {(hasNewsletterExcerpt && showExcerpt) && ( -

    A subtitle to highlight key points and engage your readers

    - )}
    -

    +

    By {authorPlaceholder} {currentDate}

    -

    View in browser

    +

    View in browser

    )} @@ -173,21 +147,14 @@ const NewsletterPreviewContent: React.FC<{ {/* Feature image */} {showFeatureImage && ( <> -
    +
    Feature
    -
    Feature image caption
    +
    Feature image caption
    )} -
    +

    This is what your content will look like when you send one of your posts as an email newsletter to your subscribers.

    Over there on the right you'll see some settings that allow you to customize the look and feel of this template to make it perfectly suited to your brand. Email templates are exceptionally finnicky to make, but we've spent a long time optimising this one to make it work beautifully across devices, email clients and content types.

    So, you can trust that every email you send with Ghost will look great and work well. Just like the rest of your site.

    @@ -232,7 +199,7 @@ const NewsletterPreviewContent: React.FC<{

    The three latest posts published on your site

    -

    Posts sent as an email only will never be shown here.

    +

    Posts sent as an email only will never be shown here.

    Latest post @@ -241,7 +208,7 @@ const NewsletterPreviewContent: React.FC<{

    Displayed at the bottom of each newsletter

    -

    Giving your readers one more place to discover your stories.

    +

    Giving your readers one more place to discover your stories.

    Latest post @@ -250,7 +217,7 @@ const NewsletterPreviewContent: React.FC<{

    To keep your work front and center

    -

    Making sure that your audience stays engaged.

    +

    Making sure that your audience stays engaged.

    Latest post @@ -263,7 +230,7 @@ const NewsletterPreviewContent: React.FC<{ {showSubscriptionDetails && (

    Subscription details

    -

    You are receiving this because you are a paid subscriber to {siteTitle}. Your subscription will renew on 17 Jul 2024.

    +

    You are receiving this because you are a paid subscriber to The Local Host. Your subscription will renew on 17 Jul 2024.

    Name: Jamie Larson

    @@ -279,9 +246,9 @@ const NewsletterPreviewContent: React.FC<{ {/* Footer */}
    -
    +
    -
    +
    {siteTitle} © {currentYear} — Unsubscribe
    diff --git a/apps/admin-x-settings/src/components/settings/email/useDefaultRecipientsOptions.tsx b/apps/admin-x-settings/src/components/settings/email/useDefaultRecipientsOptions.tsx index 8ee3e4af38a..731b8dc6459 100644 --- a/apps/admin-x-settings/src/components/settings/email/useDefaultRecipientsOptions.tsx +++ b/apps/admin-x-settings/src/components/settings/email/useDefaultRecipientsOptions.tsx @@ -62,11 +62,11 @@ const useDefaultRecipientsOptions = (selectedOption: string, defaultEmailRecipie const initSelectedSegments = async () => { const filters = defaultEmailRecipientsFilter?.split(',') || []; - const tierIds: string[] = [], labelSlugs: string[] = [], offerIds: string[] = []; + const tierIds: string[] = [], labelIds: string[] = [], offerIds: string[] = []; for (const filter of filters) { if (filter.startsWith('label:')) { - labelSlugs.push(filter.replace('label:', '')); + labelIds.push(filter.replace('label:', '')); } else if (filter.startsWith('offer_redemptions:')) { offerIds.push(filter.replace('offer_redemptions:', '')); } else if (isObjectId(filter)) { @@ -75,9 +75,9 @@ const useDefaultRecipientsOptions = (selectedOption: string, defaultEmailRecipie } const options = await Promise.all([ - tiers.loadInitialValues(tierIds, 'id').then(data => data.map(tierOption)), - labels.loadInitialValues(labelSlugs, 'slug').then(data => data.map(labelOption)), - offers.loadInitialValues(offerIds, 'id').then(data => data.map(offerOption)) + tiers.loadInitialValues(tierIds).then(data => data.map(tierOption)), + labels.loadInitialValues(labelIds).then(data => data.map(labelOption)), + offers.loadInitialValues(offerIds).then(data => data.map(offerOption)) ]).then(results => results.flat()); setSelectedSegments(filters.map(filter => options.find(option => option.value === filter)!)); diff --git a/apps/admin-x-settings/src/components/settings/general/SocialAccounts.tsx b/apps/admin-x-settings/src/components/settings/general/SocialAccounts.tsx index 2516b3c7bba..37a0a057804 100644 --- a/apps/admin-x-settings/src/components/settings/general/SocialAccounts.tsx +++ b/apps/admin-x-settings/src/components/settings/general/SocialAccounts.tsx @@ -76,7 +76,7 @@ const SocialAccounts: React.FC<{ keywords: string[] }> = ({keywords}) => { { diff --git a/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx b/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx index 27e492db49f..f44833c327b 100644 --- a/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx +++ b/apps/admin-x-settings/src/components/settings/general/UserDetailModal.tsx @@ -113,6 +113,10 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { onSave: async (values) => { await updateUser?.(values); }, + onSavedStateReset: () => { + mainModal.remove(); + navigateOnClose(); + }, onSaveError: handleError }); const setUserData = (newData: User) => updateForm(() => newData); @@ -349,10 +353,9 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { animate={canAccessSettings(currentUser)} backDrop={canAccessSettings(currentUser)} buttonsDisabled={okProps.disabled} - cancelLabel='Close' dirty={saveState === 'unsaved'} okColor={okProps.color} - okLabel={okProps.label || 'Save'} + okLabel={okProps.label || 'Save & close'} size={canAccessSettings(currentUser) ? 'lg' : 'bleed'} stickyFooter={true} testId='user-detail-modal' diff --git a/apps/admin-x-settings/src/components/settings/general/Users.tsx b/apps/admin-x-settings/src/components/settings/general/Users.tsx index 703ef6547e1..aaede23f545 100644 --- a/apps/admin-x-settings/src/components/settings/general/Users.tsx +++ b/apps/admin-x-settings/src/components/settings/general/Users.tsx @@ -221,7 +221,7 @@ const Users: React.FC<{ keywords: string[], highlight?: boolean }> = ({keywords, }; const buttons = ( -
    diff --git a/apps/admin-x-settings/src/components/settings/growth/embedSignup/EmbedSignupForm.tsx b/apps/admin-x-settings/src/components/settings/growth/embedSignup/EmbedSignupForm.tsx index 703a52c622c..8dffc09960f 100644 --- a/apps/admin-x-settings/src/components/settings/growth/embedSignup/EmbedSignupForm.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/embedSignup/EmbedSignupForm.tsx @@ -11,7 +11,7 @@ const EmbedSignupForm: React.FC<{ keywords: string[] }> = ({keywords}) => { return ( } + customButtons={
    ); diff --git a/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx b/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx index 01e9e5d6f7d..2fcff193d85 100644 --- a/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx @@ -1,4 +1,4 @@ -import NiceModal from '@ebay/nice-modal-react'; +import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {useEffect, useRef} from 'react'; import TierDetailPreview from './TierDetailPreview'; import useFeatureFlag from '../../../../hooks/useFeatureFlag'; @@ -17,6 +17,7 @@ export type TierFormState = Partial> & { const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => { const isFreeTier = tier?.type === 'free'; + const modal = useModal(); const {updateRoute} = useRouting(); const {mutateAsync: updateTier} = useEditTier(); const {mutateAsync: createTier} = useAddTier(); @@ -96,6 +97,10 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => { } } }, + onSavedStateReset: () => { + modal.remove(); + updateRoute('tiers'); + }, onSaveError: handleError }); @@ -180,11 +185,10 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => { updateRoute('tiers'); }} buttonsDisabled={okProps.disabled} - cancelLabel='Close' dirty={saveState === 'unsaved'} leftButtonProps={leftButtonProps} okColor={okProps.color} - okLabel={okProps.label || 'Save'} + okLabel={okProps.label || 'Save & close'} size='lg' testId='tier-detail-modal' title={(tier ? (tier.active ? 'Edit tier' : 'Edit archived tier') : 'New tier')} diff --git a/apps/admin-x-settings/src/components/settings/site/AnnouncementBar.tsx b/apps/admin-x-settings/src/components/settings/site/AnnouncementBar.tsx index d0ecb513c0d..2bfaae75f91 100644 --- a/apps/admin-x-settings/src/components/settings/site/AnnouncementBar.tsx +++ b/apps/admin-x-settings/src/components/settings/site/AnnouncementBar.tsx @@ -11,7 +11,7 @@ const AnnouncementBar: React.FC<{ keywords: string[] }> = ({keywords}) => { return ( } + customButtons={
    -
    +
    {{#if (not (or this.loading this.error))}}
    {{#each this.entries as |entry|}} {{else}}
    @@ -26,7 +26,7 @@
    diff --git a/ghost/admin/app/components/editor/modals/publish-flow/complete.hbs b/ghost/admin/app/components/editor/modals/publish-flow/complete.hbs index 505cd3bd8e2..83c6e3116a3 100644 --- a/ghost/admin/app/components/editor/modals/publish-flow/complete.hbs +++ b/ghost/admin/app/components/editor/modals/publish-flow/complete.hbs @@ -97,7 +97,18 @@

    {{else}}

    - Back to dashboard + {{#if (feature "onboardingChecklist")}} + Back to dashboard + {{else}} + + {{/if}}

    {{/if}} {{/if}} diff --git a/ghost/admin/app/components/editor/publish-management.js b/ghost/admin/app/components/editor/publish-management.js index eb2906522b8..7d97a319064 100644 --- a/ghost/admin/app/components/editor/publish-management.js +++ b/ghost/admin/app/components/editor/publish-management.js @@ -56,7 +56,7 @@ export default class PublishManagement extends Component { } } - if (isValid && (!this.publishFlowModal || this.publishFlowModal?.isClosing)) { + if (isValid && !this.publishFlowModal || this.publishFlowModal?.isClosing) { this.publishOptions.resetPastScheduledAt(); this.publishFlowModal = this.modals.open(PublishFlowModal, { @@ -83,7 +83,7 @@ export default class PublishManagement extends Component { const isValid = await this._validatePost(); - if (isValid && (!this.updateFlowModal || this.updateFlowModal.isClosing)) { + if (isValid && !this.updateFlowModal || this.updateFlowModal.isClosing) { this.updateFlowModal = this.modals.open(UpdateFlowModal, { publishOptions: this.publishOptions, saveTask: this.publishTask @@ -99,12 +99,10 @@ export default class PublishManagement extends Component { } @action - async openPreview(event, {skipAnimation} = {}) { + openPreview(event, {skipAnimation} = {}) { event?.preventDefault(); - const isValid = await this._validatePost(); - - if (isValid && (!this.previewModal || this.previewModal.isClosing)) { + if (!this.previewModal || this.previewModal.isClosing) { // open publish flow modal underneath to offer quick switching // without restarting the flow or causing flicker diff --git a/ghost/admin/app/components/gh-alert.js b/ghost/admin/app/components/gh-alert.js index 2114ae97a10..ca22911459a 100644 --- a/ghost/admin/app/components/gh-alert.js +++ b/ghost/admin/app/components/gh-alert.js @@ -9,8 +9,8 @@ export default class GhAlert extends Component { const typeMapping = { success: 'green', error: 'red', - warn: 'black', - info: 'black' + warn: 'blue', + info: 'blue' }; const type = this.args.message.type; diff --git a/ghost/admin/app/components/gh-editor-feature-image.hbs b/ghost/admin/app/components/gh-editor-feature-image.hbs index 0d934a7d878..66775bb48d6 100644 --- a/ghost/admin/app/components/gh-editor-feature-image.hbs +++ b/ghost/admin/app/components/gh-editor-feature-image.hbs @@ -46,11 +46,11 @@ @imageSrc={{@image}} @saveImage={{fn this.saveImage uploader.setFiles}} /> -
    -
    +
    {{#if this.isEditingAlt}} - - {{#if (feature 'editorExcerpt')}} -
    - - {{#if @excerptHasTk}} -
    - TK -
    - {{/if}} -
    - {{#if @excerptErrorMessage}} -
    - {{@excerptErrorMessage}} -
    - {{/if}} -
    - {{/if}}
    diff --git a/ghost/admin/app/components/gh-koenig-editor-lexical.js b/ghost/admin/app/components/gh-koenig-editor-lexical.js index 2319ae9c8a6..abc7eb7c4bc 100644 --- a/ghost/admin/app/components/gh-koenig-editor-lexical.js +++ b/ghost/admin/app/components/gh-koenig-editor-lexical.js @@ -4,13 +4,11 @@ import {action} from '@ember/object'; import {inject as service} from '@ember/service'; import {tracked} from '@glimmer/tracking'; -export default class GhKoenigEditorLexical extends Component { +export default class GhKoenigEditorReactComponent extends Component { @service settings; - @service feature; containerElement = null; titleElement = null; - excerptElement = null; mousedownY = 0; uploadUrl = `${ghostPaths().apiRoot}/images/upload/`; @@ -32,10 +30,6 @@ export default class GhKoenigEditorLexical extends Component { return color; } - get excerpt() { - return this.args.excerpt || ''; - } - @action registerElement(element) { this.containerElement = element; @@ -112,106 +106,25 @@ export default class GhKoenigEditorLexical extends Component { this.titleElement.focus(); } + // move cursor to the editor on + // - Tab + // - Arrow Down/Right when input is empty or caret at end of input + // - Enter, creating an empty paragraph when editor is not empty @action onTitleKeydown(event) { - if (this.feature.editorExcerpt) { - // move cursor to the excerpt on - // - Tab (handled by browser) - // - Arrow Down/Right when input is empty or caret at end of input - // - Enter - const {key} = event; - const {value, selectionStart} = event.target; - - if (key === 'Enter') { - event.preventDefault(); - this.excerptElement?.focus(); - } - - if ((key === 'ArrowDown' || key === 'ArrowRight') && !event.shiftKey) { - const couldLeaveTitle = !value || selectionStart === value.length; - - if (couldLeaveTitle) { - event.preventDefault(); - this.excerptElement?.focus(); - } - } - } else { - // move cursor to the editor on - // - Tab - // - Arrow Down/Right when input is empty or caret at end of input - // - Enter, creating an empty paragraph when editor is not empty - const {editorAPI} = this; - - if (!editorAPI || event.originalEvent.isComposing) { - return; - } - - const {key} = event; - const {value, selectionStart} = event.target; - - const couldLeaveTitle = !value || selectionStart === value.length; - const arrowLeavingTitle = ['ArrowDown', 'ArrowRight'].includes(key) && couldLeaveTitle; - - if (key === 'Enter' || key === 'Tab' || arrowLeavingTitle) { - event.preventDefault(); + const {editorAPI} = this; - if (key === 'Enter' && !editorAPI.editorIsEmpty()) { - editorAPI.insertParagraphAtTop({focus: true}); - } else { - editorAPI.focusEditor({position: 'top'}); - } - } + if (!editorAPI || event.originalEvent.isComposing) { + return; } - } - - // Subtitle ("excerpt") Actions ------------------------------------------- - @action - registerExcerptElement(element) { - this.excerptElement = element; - } - - @action - focusExcerpt() { - this.excerptElement?.focus(); - - // timeout ensures this occurs after the keyboard events - setTimeout(() => { - this.excerptElement?.setSelectionRange(-1, -1); - }, 0); - } - - @action - onExcerptInput(event) { - this.args.setExcerpt?.(event.target.value); - } - - @action - onExcerptKeydown(event) { - // move cursor to the title on - // - Shift+Tab (handled by the browser) - // - Arrow Up/Left when input is empty or caret at start of input - // move cursor to the editor on - // - Tab - // - Arrow Down/Right when input is empty or caret at end of input - // - Enter, creating an empty paragraph when editor is not empty const {key} = event; const {value, selectionStart} = event.target; - if ((key === 'ArrowUp' || key === 'ArrowLeft') && !event.shiftKey) { - const couldLeaveTitle = !value || selectionStart === 0; - - if (couldLeaveTitle) { - event.preventDefault(); - this.focusTitle(); - } - } - - const {editorAPI} = this; const couldLeaveTitle = !value || selectionStart === value.length; - const arrowLeavingTitle = (key === 'ArrowRight' || key === 'ArrowDown') && couldLeaveTitle; + const arrowLeavingTitle = ['ArrowDown', 'ArrowRight'].includes(key) && couldLeaveTitle; - if (key === 'Enter' || (key === 'Tab' && !event.shiftKey) || arrowLeavingTitle) { + if (key === 'Enter' || key === 'Tab' || arrowLeavingTitle) { event.preventDefault(); if (key === 'Enter' && !editorAPI.editorIsEmpty()) { @@ -222,8 +135,6 @@ export default class GhKoenigEditorLexical extends Component { } } - // move cursor to the editor on - // Body actions ------------------------------------------------------------ @action @@ -236,7 +147,7 @@ export default class GhKoenigEditorLexical extends Component { // otherwise the browser will defocus the editor and the cursor will disappear @action focusEditor(event) { - if (!this.skipFocusEditor && event.target.classList.contains('gh-koenig-editor-pane') && this.editorAPI) { + if (!this.skipFocusEditor && event.target.classList.contains('gh-koenig-editor-pane')) { let editorCanvas = this.editorAPI.editorInstance.getRootElement(); let {bottom} = editorCanvas.getBoundingClientRect(); diff --git a/ghost/admin/app/components/gh-member-settings-form.hbs b/ghost/admin/app/components/gh-member-settings-form.hbs index d301aadf30d..f1a7630274e 100644 --- a/ghost/admin/app/components/gh-member-settings-form.hbs +++ b/ghost/admin/app/components/gh-member-settings-form.hbs @@ -147,7 +147,21 @@ {{/if}} {{/if}} - {{sub.validityDetails}} + {{#if sub.isComplimentary}} + {{#if sub.compExpiry}} + Expires {{sub.compExpiry}} + {{/if}} + {{else}} + {{#if sub.hasEnded}} + Ended {{sub.validUntil}} + {{else if sub.willEndSoon}} + Has access until {{sub.validUntil}} + {{else if sub.trialUntil}} + Ends {{sub.trialUntil}} + {{else}} + Renews {{sub.validUntil}} + {{/if}} + {{/if}}
    diff --git a/ghost/admin/app/components/gh-member-settings-form.js b/ghost/admin/app/components/gh-member-settings-form.js index bcd9193bf50..8596af3b455 100644 --- a/ghost/admin/app/components/gh-member-settings-form.js +++ b/ghost/admin/app/components/gh-member-settings-form.js @@ -1,8 +1,9 @@ import Component from '@glimmer/component'; +import moment from 'moment-timezone'; import {action} from '@ember/object'; -import {didCancel, task} from 'ember-concurrency'; -import {getSubscriptionData} from 'ghost-admin/utils/subscription-data'; +import {getNonDecimal, getSymbol} from 'ghost-admin/utils/currency'; import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; import {tracked} from '@glimmer/tracking'; export default class extends Component { @@ -59,9 +60,41 @@ export default class extends Component { return typeof value.id !== 'undefined' && self.findIndex(element => (element.tier_id || element.id) === (value.tier_id || value.id)) === index; }); - let subsWithPrice = subscriptions.filter(sub => !!sub.price); - let subscriptionData = subsWithPrice.map(sub => getSubscriptionData(sub)); + let subscriptionData = subscriptions.filter((sub) => { + return !!sub.price; + }).map((sub) => { + const periodEnded = sub.current_period_end && new Date(sub.current_period_end) < new Date(); + const data = { + ...sub, + attribution: { + ...sub.attribution, + referrerSource: sub.attribution?.referrer_source || 'Unknown', + referrerMedium: sub.attribution?.referrer_medium || '-' + }, + startDate: sub.start_date ? moment(sub.start_date).format('D MMM YYYY') : '-', + validUntil: sub.current_period_end ? moment(sub.current_period_end).format('D MMM YYYY') : '-', + hasEnded: sub.status === 'canceled' && periodEnded, + willEndSoon: sub.cancel_at_period_end || (sub.status === 'canceled' && !periodEnded), + cancellationReason: sub.cancellation_reason, + price: { + ...sub.price, + currencySymbol: getSymbol(sub.price.currency), + nonDecimalAmount: getNonDecimal(sub.price.amount) + }, + isComplimentary: !sub.id + }; + if (sub.trial_end_at) { + const inTrialMode = moment(sub.trial_end_at).isAfter(new Date(), 'day'); + if (inTrialMode) { + data.trialUntil = moment(sub.trial_end_at).format('D MMM YYYY'); + } + } + if (!sub.id && sub.tier?.expiry_at) { + data.compExpiry = moment(sub.tier.expiry_at).utc().format('D MMM YYYY'); + } + return data; + }); return tiers.map((tier) => { let tierSubscriptions = subscriptionData.filter((subscription) => { return subscription?.price?.tier?.tier_id === (tier.tier_id || tier.id); @@ -104,17 +137,8 @@ export default class extends Component { @action setup() { - try { - this.fetchTiers.perform(); - this.fetchNewsletters.perform(); - } catch (e) { - // Do not throw cancellation errors - if (didCancel(e)) { - return; - } - - throw e; - } + this.fetchTiers.perform(); + this.fetchNewsletters.perform(); } @action diff --git a/ghost/admin/app/components/gh-nav-menu/footer-banner.hbs b/ghost/admin/app/components/gh-nav-menu/footer-banner.hbs deleted file mode 100644 index 7ebaa07e970..00000000000 --- a/ghost/admin/app/components/gh-nav-menu/footer-banner.hbs +++ /dev/null @@ -1,28 +0,0 @@ -{{#if this.showReferralInvite}} - -{{/if}} - -{{#if (and this.showWhatsNew this.whatsNew.hasNew)}} - {{#let (get this.whatsNew.entries "0") as |entry|}} - - {{/let}} -{{/if}} \ No newline at end of file diff --git a/ghost/admin/app/components/gh-nav-menu/footer-banner.js b/ghost/admin/app/components/gh-nav-menu/footer-banner.js deleted file mode 100644 index fab716af821..00000000000 --- a/ghost/admin/app/components/gh-nav-menu/footer-banner.js +++ /dev/null @@ -1,89 +0,0 @@ -import Component from '@glimmer/component'; -import envConfig from 'ghost-admin/config/environment'; -import moment from 'moment-timezone'; -import {action} from '@ember/object'; -import {inject as service} from '@ember/service'; -import {task} from 'ember-concurrency'; - -export default class FooterBanner extends Component { - @service session; - @service dashboardStats; - @service feature; - @service membersUtils; - @service modals; - @service settings; - @service whatsNew; - - constructor() { - super(...arguments); - this.loadCurrentMRR.perform(); - } - - get isAdminOrOwner() { - return this.session.user.isAdmin; - } - - get isReferralNotificationNotDismissed() { - return !this.feature.accessibility.referralInviteDismissed; - } - - get stripeLiveModeEnabled() { - // allow testing mode when not in a production environment - const isDevModeStripeEnabled = envConfig.environment !== 'production' && this.membersUtils.isStripeEnabled; - const isLiveEnabled = this.settings.stripeConnectLivemode; - return isDevModeStripeEnabled || isLiveEnabled; - } - - get hasReachedMRR() { - return this.dashboardStats.currentMRR / 100 >= 100; - } - - get showReferralInvite() { - // Conditions to see the referral invite - // 1. Needs to be Owner or Admin - // 2. Stripe is setup and enabled in live mode - // 3. MRR is > $100 - // 4. Notification has not yet been dismissed by the user - return !this.args.hasThemeErrors && this.isAdminOrOwner && this.isReferralNotificationNotDismissed && this.stripeLiveModeEnabled && this.hasReachedMRR; - } - - get showWhatsNew() { - return !this.showReferralInvite && this.whatsNew.hasNewFeatured; - } - - @task - *loadCurrentMRR() { - if (this.isAdminOrOwnern) { - try { - yield this.dashboardStats.loadMrrStats(); - } catch (error) { - // noop - } - } - } - - @action - dismissReferralInvite(event) { - event.preventDefault(); - event.stopPropagation(); - - if (!this.feature.referralInviteDismissed) { - this.feature.referralInviteDismissed = moment().tz(this.settings.timezone); - } - } - - @action - dismissWhatsNewToast(event) { - event.preventDefault(); - event.stopPropagation(); - - // Dismiss - this.whatsNew.seen(); - } - - @action - openFeaturedWhatsNew(href) { - window.open(href, '_blank'); - this.whatsNew.seen(); - } -} diff --git a/ghost/admin/app/components/gh-nav-menu/footer.hbs b/ghost/admin/app/components/gh-nav-menu/footer.hbs index e40db083631..da96e851024 100644 --- a/ghost/admin/app/components/gh-nav-menu/footer.hbs +++ b/ghost/admin/app/components/gh-nav-menu/footer.hbs @@ -1,19 +1,12 @@
    {{#if this.hasThemeErrors}} -
    - -
    + {{/if}} - +
    @@ -21,7 +14,7 @@
    - {{#if (and this.whatsNew.hasNew (not this.whatsNew.hasNewFeatured))}}{{/if}} + {{#if this.whatsNew.hasNew}}{{/if}}
    {{svg-jar "arrow-down" class="w3 mr1 fill-darkgrey"}}
    @@ -52,12 +45,12 @@ {{else}}
  1. - +
  2. {{/if}} diff --git a/ghost/admin/app/components/gh-nav-menu/footer.js b/ghost/admin/app/components/gh-nav-menu/footer.js index b51777f2359..eb9e9eba1fc 100644 --- a/ghost/admin/app/components/gh-nav-menu/footer.js +++ b/ghost/admin/app/components/gh-nav-menu/footer.js @@ -1,6 +1,5 @@ import Component from '@ember/component'; import ThemeErrorsModal from '../modals/design/theme-errors'; -import WhatsNew from '../modals/whats-new'; import calculatePosition from 'ember-basic-dropdown/utils/calculate-position'; import classic from 'ember-classic-decorator'; import {action} from '@ember/object'; @@ -44,7 +43,7 @@ export default class Footer extends Component { // filter errors that have other UI to display to users that the functionality is not working const filteredErrors = errors?.filter((error) => { if (error.code === 'GS110-NO-MISSING-PAGE-BUILDER-USAGE' && error?.failures?.[0].message.includes(`show_title_and_feature_image`)) { - return false; + return false; } return true; }); @@ -61,9 +60,4 @@ export default class Footer extends Component { return {horizontalPosition, verticalPosition, style}; } - - @action - openWhatsNew() { - return this.modals.open(WhatsNew); - } } diff --git a/ghost/admin/app/components/gh-nav-menu/main.hbs b/ghost/admin/app/components/gh-nav-menu/main.hbs index 47ea632ce7d..58f9ecc8f74 100644 --- a/ghost/admin/app/components/gh-nav-menu/main.hbs +++ b/ghost/admin/app/components/gh-nav-menu/main.hbs @@ -18,7 +18,7 @@
      {{#if (gh-user-can-admin this.session.user)}} -
    • +
    • {{svg-jar "house"}} Dashboard
    • {{/if}} diff --git a/ghost/admin/app/components/gh-nav-menu/main.js b/ghost/admin/app/components/gh-nav-menu/main.js index d47997f0a33..51bce6256a8 100644 --- a/ghost/admin/app/components/gh-nav-menu/main.js +++ b/ghost/admin/app/components/gh-nav-menu/main.js @@ -23,10 +23,10 @@ export default class Main extends Component.extend(ShortcutsMixin) { @service router; @service session; @service ui; + @service whatsNew; @service membersStats; @service settings; @service explore; - @service notifications; @inject config; diff --git a/ghost/admin/app/components/gh-post-settings-menu.hbs b/ghost/admin/app/components/gh-post-settings-menu.hbs index 1d01dcc5bce..06963c9ebaf 100644 --- a/ghost/admin/app/components/gh-post-settings-menu.hbs +++ b/ghost/admin/app/components/gh-post-settings-menu.hbs @@ -93,7 +93,7 @@ {{/if}} {{/if}} - {{#unless (feature 'editorExcerpt')}} + - {{/unless}} {{#unless this.session.user.isAuthorOrContributor}} diff --git a/ghost/admin/app/components/gh-token-input/label-token.js b/ghost/admin/app/components/gh-token-input/label-token.js index 0327c9c3924..d82786d0681 100644 --- a/ghost/admin/app/components/gh-token-input/label-token.js +++ b/ghost/admin/app/components/gh-token-input/label-token.js @@ -1,17 +1,10 @@ import DraggableObject from 'ember-drag-drop/components/draggable-object'; import classic from 'ember-classic-decorator'; -import {alias} from '@ember/object/computed'; import {attributeBindings, classNames} from '@ember-decorators/component'; -import {computed} from '@ember/object'; @classic @attributeBindings('title') @classNames('label-token') export default class LabelToken extends DraggableObject { - @alias('content.name') name; - - @computed('name') - get title() { - return this.name ?? 'Label'; - } + title = this.name ?? 'Label'; } diff --git a/ghost/admin/app/components/koenig-image-editor.hbs b/ghost/admin/app/components/koenig-image-editor.hbs index f58b2a165e0..f5180629176 100644 --- a/ghost/admin/app/components/koenig-image-editor.hbs +++ b/ghost/admin/app/components/koenig-image-editor.hbs @@ -6,10 +6,10 @@ {{/if}} diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 18e6d2df19c..da1770b383d 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -83,28 +83,6 @@ export function decoratePostSearchResult(item, settings) { } } -/** - * Fetches the URLs of all active offers - * @returns {Promise<{label: string, value: string}[]>} - */ -export async function offerUrls() { - let offers = []; - - try { - offers = await this.fetchOffersTask.perform(); - } catch (e) { - // No-op: if offers are not available (e.g. missing permissions), return an empty array - return []; - } - - return offers.map((offer) => { - return { - label: `Offer — ${offer.name}`, - value: this.config.getSiteUrl(offer.code) - }; - }); -} - class ErrorHandler extends React.Component { state = { hasError: false @@ -295,6 +273,8 @@ export default class KoenigLexicalEditor extends Component { }; const fetchAutocompleteLinks = async () => { + const offers = await this.fetchOffersTask.perform(); + const defaults = [ {label: 'Homepage', value: window.location.origin + '/'}, {label: 'Free signup', value: '#/portal/signup/free'} @@ -339,23 +319,18 @@ export default class KoenigLexicalEditor extends Component { return []; }; - const offersLinks = await offerUrls.call(this); + const offersLinks = offers.toArray().map((offer) => { + return { + label: `Offer - ${offer.name}`, + value: this.config.getSiteUrl(offer.code) + }; + }); return [...defaults, ...memberLinks(), ...donationLink(), ...recommendationLink(), ...offersLinks]; }; const fetchLabels = async () => { - let labels = []; - try { - labels = await this.fetchLabelsTask.perform(); - } catch (e) { - // Do not throw cancellation errors - if (didCancel(e)) { - return; - } - - throw e; - } + const labels = await this.fetchLabelsTask.perform(); return labels.map(label => label.name); }; @@ -397,21 +372,12 @@ export default class KoenigLexicalEditor extends Component { if (!didCancel(error)) { throw error; } - return; } - // only published posts/pages and staff with posts have URLs + // only published posts/pages have URLs const filteredResults = []; results.forEach((group) => { - let items = group.options; - - if (group.groupName === 'Posts' || group.groupName === 'Pages') { - items = items.filter(i => i.status === 'published'); - } - - if (group.groupName === 'Staff') { - items = items.filter(i => !/\/404\//.test(i.url)); - } + const items = (group.groupName === 'Posts' || group.groupName === 'Pages') ? group.options.filter(i => i.status === 'published') : group.options; if (items.length === 0) { return; @@ -448,22 +414,18 @@ export default class KoenigLexicalEditor extends Component { fetchCollectionPosts, fetchEmbed, fetchLabels, - renderLabels: !this.session.user.isContributor, feature: { collectionsCard: this.feature.collectionsCard, collections: this.feature.collections, - internalLinking: this.feature.internalLinking, - internalLinkingAtLinks: this.feature.internalLinking, - contentVisibility: this.feature.contentVisibility + internalLinking: this.feature.internalLinking }, - deprecated: { // todo fix typo + deprecated: { headerV1: true // if false, shows header v1 in the menu }, membersEnabled: this.settings.membersSignupAccess === 'all', searchLinks, siteTitle: this.settings.title, - siteDescription: this.settings.description, - siteUrl: this.config.getSiteUrl('/') + siteDescription: this.settings.description }; const cardConfig = Object.assign({}, defaultCardConfig, props.cardConfig, {pinturaConfig: this.pinturaConfig}); diff --git a/ghost/admin/app/components/members-activity/event-type-filter.js b/ghost/admin/app/components/members-activity/event-type-filter.js index 50029841c67..957eafeec0c 100644 --- a/ghost/admin/app/components/members-activity/event-type-filter.js +++ b/ghost/admin/app/components/members-activity/event-type-filter.js @@ -11,8 +11,7 @@ const ALL_EVENT_TYPES = [ {event: 'email_opened_event', icon: 'filter-dropdown-email-opened', name: 'Email opened', group: 'emails'}, {event: 'email_delivered_event', icon: 'filter-dropdown-email-received', name: 'Email received', group: 'emails'}, {event: 'email_complaint_event', icon: 'filter-dropdown-email-flagged-as-spam', name: 'Email flagged as spam', group: 'emails'}, - {event: 'email_failed_event', icon: 'filter-dropdown-email-bounced', name: 'Email bounced', group: 'emails'}, - {event: 'email_change_event', icon: 'filter-dropdown-email-address-changed', name: 'Email address changed', group: 'emails'} + {event: 'email_failed_event', icon: 'filter-dropdown-email-bounced', name: 'Email bounced', group: 'emails'} ]; export default class MembersActivityEventTypeFilter extends Component { diff --git a/ghost/admin/app/components/members/filter.js b/ghost/admin/app/components/members/filter.js index 6993d1c4e79..475ddf53eb7 100644 --- a/ghost/admin/app/components/members/filter.js +++ b/ghost/admin/app/components/members/filter.js @@ -4,8 +4,8 @@ import nql from '@tryghost/nql-lang'; import {AUDIENCE_FEEDBACK_FILTER, CREATED_AT_FILTER, EMAIL_CLICKED_FILTER, EMAIL_COUNT_FILTER, EMAIL_FILTER, EMAIL_OPENED_COUNT_FILTER, EMAIL_OPENED_FILTER, EMAIL_OPEN_RATE_FILTER, EMAIL_SENT_FILTER, LABEL_FILTER, LAST_SEEN_FILTER, NAME_FILTER, NEWSLETTERS_FILTERS, NEXT_BILLING_DATE_FILTER, OFFERS_FILTER, PLAN_INTERVAL_FILTER, SIGNUP_ATTRIBUTION_FILTER, STATUS_FILTER, SUBSCRIBED_FILTER, SUBSCRIPTION_ATTRIBUTION_FILTER, SUBSCRIPTION_START_DATE_FILTER, SUBSCRIPTION_STATUS_FILTER, TIER_FILTER} from './filters'; import {TrackedArray} from 'tracked-built-ins'; import {action} from '@ember/object'; -import {didCancel, task} from 'ember-concurrency'; import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; import {tracked} from '@glimmer/tracking'; function escapeNqlString(value) { @@ -234,19 +234,9 @@ export default class MembersFilter extends Component { async parseDefaultFilters() { // we need to make sure all the filters are loaded before parsing the default filter // otherwise the filter will be parsed with the wrong properties - try { - await this.fetchTiers.perform(); - await this.fetchNewsletters.perform(); - await this.fetchOffers.perform(); - } catch (e) { - // Do not throw cancellation errors - if (didCancel(e)) { - return; - } - - throw e; - } - + await this.fetchTiers.perform(); + await this.fetchNewsletters.perform(); + await this.fetchOffers.perform(); if (this.args.defaultFilterParam) { // check if it is different before parsing const validFilters = this.validFilters; diff --git a/ghost/admin/app/components/modal-member-tier.js b/ghost/admin/app/components/modal-member-tier.js index 733d08e3e6b..bc94dc2d1ee 100644 --- a/ghost/admin/app/components/modal-member-tier.js +++ b/ghost/admin/app/components/modal-member-tier.js @@ -1,8 +1,8 @@ import ModalComponent from 'ghost-admin/components/modal-base'; import moment from 'moment-timezone'; import {action} from '@ember/object'; -import {didCancel, task} from 'ember-concurrency'; import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; import {tracked} from '@glimmer/tracking'; export default class ModalMemberTier extends ModalComponent { @@ -77,16 +77,7 @@ export default class ModalMemberTier extends ModalComponent { @action setup() { this.loadingTiers = true; - try { - this.fetchTiers.perform(); - } catch (e) { - // Do not throw cancellation errors - if (didCancel(e)) { - return; - } - - throw e; - } + this.fetchTiers.perform(); } @action diff --git a/ghost/admin/app/components/modal-post-history.hbs b/ghost/admin/app/components/modal-post-history.hbs index 3989c29db59..4795ea81032 100644 --- a/ghost/admin/app/components/modal-post-history.hbs +++ b/ghost/admin/app/components/modal-post-history.hbs @@ -1,6 +1,6 @@ {{!-- template-lint-disable no-invalid-interactive --}}
      -
      +
      {{#if this.selectedHTML}} {{{this.selectedHTML}}} @@ -9,31 +9,15 @@
      {{#if this.selectedRevision.feature_image}} - {{this.selectedRevision.feature_image_alt}} + {{/if}} {{#if this.selectedRevision.feature_image_caption}} -

      {{{this.selectedRevision.feature_image_caption}}}

      +

      {{{this.selectedRevision.feature_image_caption}}}

      {{/if}}
      -
      - {{this.currentTitle}} -
      - {{#if (feature "editorExcerpt")}} -
      - {{this.selectedRevision.custom_excerpt}} -
      -
      - {{/if}} - +
      {{this.currentTitle}}
      +
      @@ -44,8 +28,7 @@ aria-label="Close meta data panel" class="back settings-menu-header-action" data-test-button="close-psm-subview" - type="button" - {{on "click" this.closeModal}} + type="button" {{action "closeModal"}} {{on "mousedown" (optional this.noop)}} > {{svg-jar "arrow-left"}} @@ -56,19 +39,19 @@