From 77af93be6a7bc6403b42ee2e4d34a69fab281c6a Mon Sep 17 00:00:00 2001 From: Sag Date: Mon, 20 Jan 2025 22:12:55 +0700 Subject: [PATCH 01/27] =?UTF-8?q?=F0=9F=94=92=20Blocked=20spammy=20email?= =?UTF-8?q?=20domains=20in=20member=20signups=20(#22027)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ref https://linear.app/ghost/issue/ONC-721 ref https://app.incident.io/ghost/incidents/132 - added a blocklist at the email domain level for free member signups - for example, if `blocked-domain.com` is blocked, `thomas@blocked-domain.com` cannot sign up as free member - the blocklist is configurable: `"spam.blocked_email_domains": ["blocked-domain.com"]` --- apps/portal/src/utils/errors.js | 1 + .../core/core/server/services/members/api.js | 4 +- .../newsletters/NewslettersService.js | 4 +- .../services/settings/SettingsBREADService.js | 4 +- ghost/core/core/shared/config/defaults.json | 3 +- .../send-magic-link.test.js.snap | 18 +++++++ .../e2e-api/members/send-magic-link.test.js | 20 ++++++++ ghost/i18n/locales/af/portal.json | 1 + ghost/i18n/locales/ar/portal.json | 1 + ghost/i18n/locales/bg/portal.json | 1 + ghost/i18n/locales/bn/portal.json | 1 + ghost/i18n/locales/bs/portal.json | 1 + ghost/i18n/locales/ca/portal.json | 1 + ghost/i18n/locales/context.json | 1 + ghost/i18n/locales/cs/portal.json | 1 + ghost/i18n/locales/da/portal.json | 1 + ghost/i18n/locales/de-CH/portal.json | 1 + ghost/i18n/locales/de/portal.json | 1 + ghost/i18n/locales/el/portal.json | 1 + ghost/i18n/locales/en/portal.json | 1 + ghost/i18n/locales/eo/portal.json | 1 + ghost/i18n/locales/es/portal.json | 1 + ghost/i18n/locales/et/portal.json | 1 + ghost/i18n/locales/fa/portal.json | 1 + ghost/i18n/locales/fi/portal.json | 1 + ghost/i18n/locales/fr/portal.json | 1 + ghost/i18n/locales/gd/portal.json | 1 + ghost/i18n/locales/he/portal.json | 1 + ghost/i18n/locales/hi/portal.json | 1 + ghost/i18n/locales/hr/portal.json | 1 + ghost/i18n/locales/hu/portal.json | 1 + ghost/i18n/locales/id/portal.json | 1 + ghost/i18n/locales/is/portal.json | 1 + ghost/i18n/locales/it/portal.json | 1 + ghost/i18n/locales/ja/portal.json | 1 + ghost/i18n/locales/ko/portal.json | 1 + ghost/i18n/locales/kz/portal.json | 1 + ghost/i18n/locales/lt/portal.json | 1 + ghost/i18n/locales/lv/portal.json | 1 + ghost/i18n/locales/mk/portal.json | 1 + ghost/i18n/locales/mn/portal.json | 1 + ghost/i18n/locales/ms/portal.json | 1 + ghost/i18n/locales/ne/portal.json | 1 + ghost/i18n/locales/nl/portal.json | 1 + ghost/i18n/locales/nn/portal.json | 1 + ghost/i18n/locales/no/portal.json | 1 + ghost/i18n/locales/pl/portal.json | 1 + ghost/i18n/locales/pt-BR/portal.json | 1 + ghost/i18n/locales/pt/portal.json | 1 + ghost/i18n/locales/ro/portal.json | 1 + ghost/i18n/locales/ru/portal.json | 1 + ghost/i18n/locales/si/portal.json | 1 + ghost/i18n/locales/sk/portal.json | 1 + ghost/i18n/locales/sl/portal.json | 1 + ghost/i18n/locales/sq/portal.json | 1 + ghost/i18n/locales/sr-Cyrl/portal.json | 1 + ghost/i18n/locales/sr/portal.json | 1 + ghost/i18n/locales/sv/portal.json | 1 + ghost/i18n/locales/sw/portal.json | 1 + ghost/i18n/locales/ta/portal.json | 1 + ghost/i18n/locales/th/portal.json | 1 + ghost/i18n/locales/tr/portal.json | 1 + ghost/i18n/locales/uk/portal.json | 1 + ghost/i18n/locales/ur/portal.json | 1 + ghost/i18n/locales/uz/portal.json | 1 + ghost/i18n/locales/vi/portal.json | 1 + ghost/i18n/locales/zh-Hant/portal.json | 1 + ghost/i18n/locales/zh/portal.json | 1 + ghost/magic-link/lib/MagicLink.js | 32 +++++++++++- ghost/magic-link/test/index.test.js | 51 +++++++++++++++++++ ghost/members-api/lib/members-api.js | 6 ++- 71 files changed, 196 insertions(+), 8 deletions(-) diff --git a/apps/portal/src/utils/errors.js b/apps/portal/src/utils/errors.js index c48e3716bfc8..93034e1cc981 100644 --- a/apps/portal/src/utils/errors.js +++ b/apps/portal/src/utils/errors.js @@ -59,6 +59,7 @@ export function chooseBestErrorMessage(error, alreadyTranslatedDefaultMessage, t t('Too many different sign-in attempts, try again in {{number}} days'); t('Failed to send magic link email'); t('This site only accepts paid members.'); + t('This email domain is not accepted, try again with a different email address'); } }; diff --git a/ghost/core/core/server/services/members/api.js b/ghost/core/core/server/services/members/api.js index 6b65cf6bd8f1..26f5d46d7caf 100644 --- a/ghost/core/core/server/services/members/api.js +++ b/ghost/core/core/server/services/members/api.js @@ -20,6 +20,7 @@ const memberAttributionService = require('../member-attribution'); const emailSuppressionList = require('../email-suppression-list'); const {t} = require('../i18n'); const sentry = require('../../../shared/sentry'); +const sharedConfig = require('../../../shared/config'); const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000; const MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE = 10 * 60 * 1000; @@ -238,7 +239,8 @@ function createApiInstance(config) { emailSuppressionList, settingsCache, sentry, - settingsHelpers + settingsHelpers, + config: sharedConfig }); return membersApiInstance; diff --git a/ghost/core/core/server/services/newsletters/NewslettersService.js b/ghost/core/core/server/services/newsletters/NewslettersService.js index 5247c69c93bb..bf9082e96a7a 100644 --- a/ghost/core/core/server/services/newsletters/NewslettersService.js +++ b/ghost/core/core/server/services/newsletters/NewslettersService.js @@ -6,6 +6,7 @@ const debug = require('@tryghost/debug')('services:newsletters'); const tpl = require('@tryghost/tpl'); const errors = require('@tryghost/errors'); const sentry = require('../../../shared/sentry'); +const config = require('../../../shared/config'); const messages = { nameAlreadyExists: 'A newsletter with the same name already exists', @@ -86,7 +87,8 @@ class NewslettersService { getText, getHTML, getSubject, - sentry + sentry, + config }); } diff --git a/ghost/core/core/server/services/settings/SettingsBREADService.js b/ghost/core/core/server/services/settings/SettingsBREADService.js index 0638920c510a..bd93c0d7d9a4 100644 --- a/ghost/core/core/server/services/settings/SettingsBREADService.js +++ b/ghost/core/core/server/services/settings/SettingsBREADService.js @@ -6,6 +6,7 @@ const logging = require('@tryghost/logging'); const MagicLink = require('@tryghost/magic-link'); const verifyEmailTemplate = require('./emails/verify-email'); const sentry = require('../../../shared/sentry'); +const config = require('../../../shared/config'); const EMAIL_KEYS = ['members_support_address']; const messages = { @@ -82,7 +83,8 @@ class SettingsBREADService { getText, getHTML, getSubject, - sentry + sentry, + config }); } diff --git a/ghost/core/core/shared/config/defaults.json b/ghost/core/core/shared/config/defaults.json index 049665626498..2c1220b8d814 100644 --- a/ghost/core/core/shared/config/defaults.json +++ b/ghost/core/core/shared/config/defaults.json @@ -126,7 +126,8 @@ "maxWait": 360000, "lifetime": 3600, "freeRetries": 10 - } + }, + "blocked_email_domains": [] }, "caching": { "frontend": { diff --git a/ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap b/ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap index e8e0a5499ec3..1305fc1af3d1 100644 --- a/ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap +++ b/ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap @@ -107,3 +107,21 @@ Object { ], } `; + +exports[`sendMagicLink blocks signups from blocked email domains 1: [body] 1`] = ` +Object { + "errors": Array [ + Object { + "code": null, + "context": null, + "details": null, + "ghostErrorCode": null, + "help": null, + "id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "message": "This email domain is not accepted, try again with a different email address", + "property": null, + "type": "BadRequestError", + }, + ], +} +`; diff --git a/ghost/core/test/e2e-api/members/send-magic-link.test.js b/ghost/core/test/e2e-api/members/send-magic-link.test.js index f353f37fb133..88eefe3869d0 100644 --- a/ghost/core/test/e2e-api/members/send-magic-link.test.js +++ b/ghost/core/test/e2e-api/members/send-magic-link.test.js @@ -4,6 +4,7 @@ const settingsCache = require('../../../core/shared/settings-cache'); const DomainEvents = require('@tryghost/domain-events'); const {anyErrorId} = matchers; const spamPrevention = require('../../../core/server/web/shared/middleware/api/spam-prevention'); +const configUtils = require('../../utils/configUtils'); let membersAgent, membersService; @@ -29,6 +30,7 @@ describe('sendMagicLink', function () { afterEach(function () { mockManager.restore(); + configUtils.restore(); }); it('Errors when passed multiple emails', async function () { @@ -285,4 +287,22 @@ describe('sendMagicLink', function () { } }); }); + + it('blocks signups from blocked email domains', async function () { + configUtils.set('spam:blocked_email_domains', ['blocked-domain.com']); + + const email = 'this-member-does-not-exist@blocked-domain.com'; + await membersAgent.post('/api/send-magic-link') + .body({ + email, + emailType: 'signup' + }) + .expectStatus(400) + .matchBodySnapshot({ + errors: [{ + id: anyErrorId, + message: 'This email domain is not accepted, try again with a different email address' + }] + }); + }); }); diff --git a/ghost/i18n/locales/af/portal.json b/ghost/i18n/locales/af/portal.json index bacfa41c0daf..90cc19596033 100644 --- a/ghost/i18n/locales/af/portal.json +++ b/ghost/i18n/locales/af/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Hierdie webwerf is slegs op uitnodiging, kontak die eienaar vir toegang.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/ar/portal.json b/ghost/i18n/locales/ar/portal.json index 2be918d4fe97..e781b56499e0 100644 --- a/ghost/i18n/locales/ar/portal.json +++ b/ghost/i18n/locales/ar/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": ".حدث خطأ أثناء الاشتراك، يرجى المحاولة مرة أخرى", "There was an error processing your payment. Please try again.": ".حدث خطأ أثناء معالجة دفعك، يرجى المحاولة مرة أخرى", "There was an error sending the email, please try again": ".حدث خطأ أثناء إرسال البريد الاكتروني، يرجى المحاولة مرة أخرى", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "هذا الموقع للمشتركين فقط، تواصل مع ادارة الموقع للحصول على اشتراك.", "This site is not accepting payments at the moment.": "هذا الموقع لا يقبل المدفوعات في الوقت الحالي", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/bg/portal.json b/ghost/i18n/locales/bg/portal.json index 6df78718e14d..3960c1dd0f2a 100644 --- a/ghost/i18n/locales/bg/portal.json +++ b/ghost/i18n/locales/bg/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Възникна грешка при продължаването на абонамента ви, опитайте отново.", "There was an error processing your payment. Please try again.": "Възникна грешка при обработката на вашето плащане. Моля, опитайте отново.", "There was an error sending the email, please try again": "Възникна грешка при изпращане на имейл, опитайте отново", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Сайтът е само с покани. Свържете се със собственика за да получите достъп.", "This site is not accepting payments at the moment.": "В момента сайтът не приема плащания.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/bn/portal.json b/ghost/i18n/locales/bn/portal.json index 4dbf4485a4e7..a89cfc896826 100644 --- a/ghost/i18n/locales/bn/portal.json +++ b/ghost/i18n/locales/bn/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "এই সাইটটি কেবল আমন্ত্রণের মাধ্যমে, প্রবেশাধিকার পেতে মালিকের সাথে যোগাযোগ করুন।", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/bs/portal.json b/ghost/i18n/locales/bs/portal.json index 5d2bfa3bd848..0d2da7661caa 100644 --- a/ghost/i18n/locales/bs/portal.json +++ b/ghost/i18n/locales/bs/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ova je stranica samo na poziv, kontaktiraj vlasnika za pristup.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/ca/portal.json b/ghost/i18n/locales/ca/portal.json index d17417d828a9..16f7031facd2 100644 --- a/ghost/i18n/locales/ca/portal.json +++ b/ghost/i18n/locales/ca/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Aquest llog és només per invitació, contacta amb el propietari per obtenir accés.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/context.json b/ghost/i18n/locales/context.json index c9ae0468fde7..9d845830c9cf 100644 --- a/ghost/i18n/locales/context.json +++ b/ghost/i18n/locales/context.json @@ -242,6 +242,7 @@ "This comment has been hidden.": "Text for a comment thas was hidden", "This comment has been removed.": "Text for a comment thas was removed", "This email address will not be used.": "This is in the footer of signup verification emails, and comes right after 'If you did not make this request, you can simply delete this message.'", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "A message on the member login screen indicating that a site is not-open to public signups", "This site is not accepting payments at the moment.": "An error message shown when a tips or donations link is opened but the site has donations disabled", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/cs/portal.json b/ghost/i18n/locales/cs/portal.json index 36f707e7d81b..f4a806b16078 100644 --- a/ghost/i18n/locales/cs/portal.json +++ b/ghost/i18n/locales/cs/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Při zpracování vaší platby došlo k chybě. Zkuste to prosím znovu.", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Tento web je pouze pro pozvané, kontaktujte provozovatele pro přístup.", "This site is not accepting payments at the moment.": "Tento web momentálně nepřijímá platby.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/da/portal.json b/ghost/i18n/locales/da/portal.json index b8f3f329f89c..03c1eb8c7880 100644 --- a/ghost/i18n/locales/da/portal.json +++ b/ghost/i18n/locales/da/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Der opstod en fejl under forlængelsen af dit abonnement, prøv venligst igen.", "There was an error processing your payment. Please try again.": "Der opstod en fejl under behandlingen af din betaling. Prøv venligst igen.", "There was an error sending the email, please try again": "Der opstod en fejl under afsendelse af e-mailen, prøv venligst igen.", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Denne sider kræver at du skal være inviteret. Kontakt ejeres for at få adgang.", "This site is not accepting payments at the moment.": "Denne side accepterer ikke betalinger i øjeblikket.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/de-CH/portal.json b/ghost/i18n/locales/de-CH/portal.json index 8d3bb9548256..03895b4d89de 100644 --- a/ghost/i18n/locales/de-CH/portal.json +++ b/ghost/i18n/locales/de-CH/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Der Zugang zu diesem Inhalt ist eingeschränkt. Bitte kontaktieren Sie uns, wenn Sie Zugang wünschen.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/de/portal.json b/ghost/i18n/locales/de/portal.json index 83d7591b7d47..7fe5353ff085 100644 --- a/ghost/i18n/locales/de/portal.json +++ b/ghost/i18n/locales/de/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Beim Erneuern deines Abonnements ist ein Fehler aufgetreten. Bitte versuche es erneut.", "There was an error processing your payment. Please try again.": "Bei der Verarbeitung deiner Zahlung gab es einen Fehler. Bitte versuche es noch einmal.", "There was an error sending the email, please try again": "Beim Versand der E-Mail ist ein Fehler aufgetreten. Bitte versuche es erneut.", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Für diese Seite benötigst du eine Einladung. Bitte kontaktiere den Inhaber.", "This site is not accepting payments at the moment.": "Diese Website nimmt zur Zeit keine Zahlungen entgegen.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/el/portal.json b/ghost/i18n/locales/el/portal.json index bc56a279929d..98a6143e0b49 100644 --- a/ghost/i18n/locales/el/portal.json +++ b/ghost/i18n/locales/el/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Αυτός ο ιστότοπος είναι μόνο με πρόσκληση, επικοινωνήστε με τον ιδιοκτήτη για πρόσβαση.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/en/portal.json b/ghost/i18n/locales/en/portal.json index 54638995cf52..508f16c34662 100644 --- a/ghost/i18n/locales/en/portal.json +++ b/ghost/i18n/locales/en/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/eo/portal.json b/ghost/i18n/locales/eo/portal.json index 760b2ebe312b..7f5724bdf9a5 100644 --- a/ghost/i18n/locales/eo/portal.json +++ b/ghost/i18n/locales/eo/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ĉi tiu retejo estas nur por invitiĝuloj, kontaktu la proprietulo por alireblo.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/es/portal.json b/ghost/i18n/locales/es/portal.json index 5e50595327c1..0dd42ceae6f9 100644 --- a/ghost/i18n/locales/es/portal.json +++ b/ghost/i18n/locales/es/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Hubo un error en continuar la suscripción, inténtalo de nuevo por favor.", "There was an error processing your payment. Please try again.": "Hubo un error procesando tu pago. Intentalo de nuevvo por favor.", "There was an error sending the email, please try again": "Hubo un error enviando el correo electrónico, intentalo de nuevo por favor.", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Este sitio es solo por invitación, contacta al propietario para obtener acceso.", "This site is not accepting payments at the moment.": "Este sitio no acepta pagos en este momento.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/et/portal.json b/ghost/i18n/locales/et/portal.json index 7eb3a3485d19..5d70c09a7db1 100644 --- a/ghost/i18n/locales/et/portal.json +++ b/ghost/i18n/locales/et/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "See sait on ainult kutsetega, juurdepääsu saamiseks võtke ühendust omanikuga.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/fa/portal.json b/ghost/i18n/locales/fa/portal.json index c73468136c5f..c8d227e1b70b 100644 --- a/ghost/i18n/locales/fa/portal.json +++ b/ghost/i18n/locales/fa/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "دسترسی به این وب\u200cسایت نیازمند دعوت\u200cنامه است، با مالک آن برای دریافت دسترسی تماس بگیرید.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/fi/portal.json b/ghost/i18n/locales/fi/portal.json index 62ec5e3b4ed2..a7714935494d 100644 --- a/ghost/i18n/locales/fi/portal.json +++ b/ghost/i18n/locales/fi/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Tämä sivu on vain kutsutuille, ota yhteyttä omistajaan saadaksesi pääsyoikeuden.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/fr/portal.json b/ghost/i18n/locales/fr/portal.json index 22f7eb2a2287..21e6f2b607e5 100644 --- a/ghost/i18n/locales/fr/portal.json +++ b/ghost/i18n/locales/fr/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Une erreur s'est produite lors de la prolongation de votre abonnement, veuillez réessayer.", "There was an error processing your payment. Please try again.": "Une erreur s'est produite lors du traitement de votre paiement. Veuillez réessayer.", "There was an error sending the email, please try again": "Une erreur s'est produite lors de l'envoi de l'e-mail, veuillez réessayer.", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ce site est réservé aux invités. Veuillez écrire au propriétaire pour en demander l'accès.", "This site is not accepting payments at the moment.": "Ce site n'accepte pas les paiements pour le moment.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/gd/portal.json b/ghost/i18n/locales/gd/portal.json index 90092761438b..007908c4babd 100644 --- a/ghost/i18n/locales/gd/portal.json +++ b/ghost/i18n/locales/gd/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Thachair mearachd fhad 's a bhathar a' làimhseachadh a' phàighidh agad. Feuch a-rithist an ceann greis.", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Feumar cuireadh airson an làrach-lìn seo, leig fios dhan rianaire ma tha thu ag iarraidh cothrom-inntrigidh.", "This site is not accepting payments at the moment.": "Chan eil an làrach seo a' gabhail ri phàighidhean an-dràsta.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/he/portal.json b/ghost/i18n/locales/he/portal.json index 1241bbd50932..19c9163cf736 100644 --- a/ghost/i18n/locales/he/portal.json +++ b/ghost/i18n/locales/he/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "שגיאה בהמשך המנוי שלכם, נסו שוב.", "There was an error processing your payment. Please try again.": "שגיאה בעיבוד התשלום שלכם. נסו שוב.", "There was an error sending the email, please try again": "שגיאה בשליחת המייל, נסו שוב", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "אתר זה פתוח למוזמנים בלבד, פנו לבעל האתר לגישה.", "This site is not accepting payments at the moment.": "אתר זה לא מקבל תשלומים כרגע.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/hi/portal.json b/ghost/i18n/locales/hi/portal.json index e5c6927dcabf..c3cb361240eb 100644 --- a/ghost/i18n/locales/hi/portal.json +++ b/ghost/i18n/locales/hi/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "यह साइट केवल निमंत्रण द्वारा है, पहुँच के लिए मालिक से संपर्क करें।", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/hr/portal.json b/ghost/i18n/locales/hr/portal.json index 69135f7de322..937fb337dd95 100644 --- a/ghost/i18n/locales/hr/portal.json +++ b/ghost/i18n/locales/hr/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ove stranice su samo za članove, kontaktirajte vlasnika kako biste dobili pristup.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/hu/portal.json b/ghost/i18n/locales/hu/portal.json index de9f7d87bd3e..f304651bcbbe 100644 --- a/ghost/i18n/locales/hu/portal.json +++ b/ghost/i18n/locales/hu/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "A website csak meghívóval látogatható. Meghívóért lépjen kapcsolatba az oldal tulajdonosával!", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/id/portal.json b/ghost/i18n/locales/id/portal.json index bd9c652e0832..5de578cae554 100644 --- a/ghost/i18n/locales/id/portal.json +++ b/ghost/i18n/locales/id/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Terjadi kesalahan saat melanjutkan langganan Anda, harap coba lagi.", "There was an error processing your payment. Please try again.": "Terjadi kesalahan saat memproses pembayaran Anda. Harap coba lagi.", "There was an error sending the email, please try again": "Terjadi kesalahan saat mengirim email, harap coba lagi", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Situs ini hanya untuk yang diundang, hubungi pemiliknya untuk mendapatkan akses.", "This site is not accepting payments at the moment.": "Situs ini tidak menerima pembayaran saat ini.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/is/portal.json b/ghost/i18n/locales/is/portal.json index ef6598309e01..9b5b2a17b8bf 100644 --- a/ghost/i18n/locales/is/portal.json +++ b/ghost/i18n/locales/is/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Aðgangur krefst boðsmiða, hafið samband við eiganda síðunnar til að fá aðgang.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/it/portal.json b/ghost/i18n/locales/it/portal.json index c99667b53316..2e5738f4051a 100644 --- a/ghost/i18n/locales/it/portal.json +++ b/ghost/i18n/locales/it/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "C'è stato un errore nella continuazione del tuo abbonamento, riprova per favore.", "There was an error processing your payment. Please try again.": "C'è stato un errore durante l’elaborazione del tuo pagamento. Riprova per favore.", "There was an error sending the email, please try again": "C'è stato un errore nell'invio dell'e-mail, per favore riprova", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Questo sito è accessibile solo su invito, contatta il proprietario per poter accedere.", "This site is not accepting payments at the moment.": "Questo sito non accetta pagamenti al momento.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/ja/portal.json b/ghost/i18n/locales/ja/portal.json index 4e9b53d0f182..9b3182cca723 100644 --- a/ghost/i18n/locales/ja/portal.json +++ b/ghost/i18n/locales/ja/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "このサイトは招待制です。アクセスするにはオーナーに連絡してください。", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/ko/portal.json b/ghost/i18n/locales/ko/portal.json index e71b36ac98ef..2fd55603b09c 100644 --- a/ghost/i18n/locales/ko/portal.json +++ b/ghost/i18n/locales/ko/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "구독 계속하기 중 오류가 발생했어요. 다시 시도해 주세요.", "There was an error processing your payment. Please try again.": "결제 처리 중 오류가 발생했어요. 다시 시도해 주세요.", "There was an error sending the email, please try again": "이메일 전송 중 오류가 발생했어요. 다시 시도해 주세요", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "위 사이트는 초대된 사용자만 사용이 가능해요. 접근을 위해서는 관리자에게 연락해 주세요.", "This site is not accepting payments at the moment.": "현재 이 사이트는 결제를 받지 않고 있어요.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/kz/portal.json b/ghost/i18n/locales/kz/portal.json index 8414afa9f024..ce82ba45e3db 100644 --- a/ghost/i18n/locales/kz/portal.json +++ b/ghost/i18n/locales/kz/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Бұл сайтқа тек шақырту бойынша кіруге болады, рұқсат алу үшін иесіне хабарласыңыз.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/lt/portal.json b/ghost/i18n/locales/lt/portal.json index d09d5df89d0e..e05538188dd1 100644 --- a/ghost/i18n/locales/lt/portal.json +++ b/ghost/i18n/locales/lt/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ši svetainė pasiekiama tik su pakvietimu, susisiekite su savininku dėl prieigos. ", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/lv/portal.json b/ghost/i18n/locales/lv/portal.json index a0b1eff46da5..190adb36072e 100644 --- a/ghost/i18n/locales/lv/portal.json +++ b/ghost/i18n/locales/lv/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Turpinot abonementu, radās kļūda. Lūdzu, mēģiniet vēlreiz.", "There was an error processing your payment. Please try again.": "Apstrādājot jūsu maksājumu, radās kļūda. Lūdzu, mēģiniet vēlreiz.", "There was an error sending the email, please try again": "Nosūtot e-pasta ziņojumu, radās kļūda. Lūdzu, mēģiniet vēlreiz", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Šī vietne ir paredzēta tikai ielūgumam. Lai iegūtu piekļuvi, sazinieties ar īpašnieku.", "This site is not accepting payments at the moment.": "Šī vietne pašlaik nepieņem maksājumus.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/mk/portal.json b/ghost/i18n/locales/mk/portal.json index 0c9831b3116d..3451e7300d6e 100644 --- a/ghost/i18n/locales/mk/portal.json +++ b/ghost/i18n/locales/mk/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Оваа страница е достапна само со покана. За пристап контактирајте го сопственикот.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/mn/portal.json b/ghost/i18n/locales/mn/portal.json index e949b2027436..97d23717539d 100644 --- a/ghost/i18n/locales/mn/portal.json +++ b/ghost/i18n/locales/mn/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Энэхүү сайт руу зөвхөн урилгаар нэвтрэх боломжтой тул та админд нь хандана уу.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/ms/portal.json b/ghost/i18n/locales/ms/portal.json index a9c86031367e..bd2111dea79d 100644 --- a/ghost/i18n/locales/ms/portal.json +++ b/ghost/i18n/locales/ms/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Laman web ini hanya untuk jemputan, hubungi pemilik untuk akses.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/ne/portal.json b/ghost/i18n/locales/ne/portal.json index 54638995cf52..508f16c34662 100644 --- a/ghost/i18n/locales/ne/portal.json +++ b/ghost/i18n/locales/ne/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/nl/portal.json b/ghost/i18n/locales/nl/portal.json index 20843b69737b..e16b198b6218 100644 --- a/ghost/i18n/locales/nl/portal.json +++ b/ghost/i18n/locales/nl/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Er was een fout bij het voortzetten van je abonnement, probeer het opnieuw.", "There was an error processing your payment. Please try again.": "Er was een fout bij het verwerken van je betaling, probeer het opnieuw.", "There was an error sending the email, please try again": "Er was een fout bij het verzenden van de e-mail, probeer het opnieuw", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Deze site is alleen toegankelijk op uitnodiging, neem contact op met de eigenaar.", "This site is not accepting payments at the moment.": "Deze site accepteert momenteel geen betalingen.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/nn/portal.json b/ghost/i18n/locales/nn/portal.json index 84f77247b85d..d3c06a17261d 100644 --- a/ghost/i18n/locales/nn/portal.json +++ b/ghost/i18n/locales/nn/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Denne sida er kun for inviterte, ta kontakt med eigaren for tilgang.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/no/portal.json b/ghost/i18n/locales/no/portal.json index 283b19c68da8..55f85c8d6460 100644 --- a/ghost/i18n/locales/no/portal.json +++ b/ghost/i18n/locales/no/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "En feil oppstod ved fornyelse av abonnementet, vennligst prøv igjen.", "There was an error processing your payment. Please try again.": "Det oppsto en feil under behandling av betalingen din. Vennligst prøv igjen.", "There was an error sending the email, please try again": "En feil oppstod ved sending av e-posten, vennligst prøv igjen", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Denne nettsiden er kun for inviterte. Kontakt eieren for invitasjon.", "This site is not accepting payments at the moment.": "Denne nettsiden godtar ikke betalinger for øyeblikket.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/pl/portal.json b/ghost/i18n/locales/pl/portal.json index 8b28b867e664..518af111ec0f 100644 --- a/ghost/i18n/locales/pl/portal.json +++ b/ghost/i18n/locales/pl/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ta strona posiada zamknięty dostęp. Skontaktuj się z właścicielem, aby uzyskać dostęp.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/pt-BR/portal.json b/ghost/i18n/locales/pt-BR/portal.json index f631eead816f..94cbdeb8c25e 100644 --- a/ghost/i18n/locales/pt-BR/portal.json +++ b/ghost/i18n/locales/pt-BR/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Houve um erro ao continuar sua assinatura, por favor, tente novamente.", "There was an error processing your payment. Please try again.": "Houve um erro ao processar seu pagamento. Por favor, tente novamente.", "There was an error sending the email, please try again": "Houve um erro ao enviar o e-mail, por favor, tente novamente.", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Este site é apenas para convidados. Contate o proprietário para obter acesso.", "This site is not accepting payments at the moment.": "Este site não está aceitando pagamentos no momento.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/pt/portal.json b/ghost/i18n/locales/pt/portal.json index 62d46bca91e8..8d7766791702 100644 --- a/ghost/i18n/locales/pt/portal.json +++ b/ghost/i18n/locales/pt/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Houve um problema ao processar o seu pagamento. Tente novamente por favor.", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "O acesso a este site é feito apenas por convite. Entre em contacto com o proprietário para obter acesso.", "This site is not accepting payments at the moment.": "Este site não está a aceitar pagamentos de momento", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/ro/portal.json b/ghost/i18n/locales/ro/portal.json index 8788c457616c..dce5d968ca71 100644 --- a/ghost/i18n/locales/ro/portal.json +++ b/ghost/i18n/locales/ro/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Acest site este disponibil doar pe bază de invitație, contactează proprietarul pentru acces.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/ru/portal.json b/ghost/i18n/locales/ru/portal.json index 4b0d228a2371..c9525ab915e5 100644 --- a/ghost/i18n/locales/ru/portal.json +++ b/ghost/i18n/locales/ru/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Произошла ошибка при обработке вашего платежа. Попробуйте ещё раз.", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Доступ к материалам этого сайта возможен только по приглашению. Для получения доступа свяжитесь с владельцем сайта.", "This site is not accepting payments at the moment.": "В данный момент сайт не принимает платежи.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/si/portal.json b/ghost/i18n/locales/si/portal.json index a5d2505aabc3..4ccedfa27b9a 100644 --- a/ghost/i18n/locales/si/portal.json +++ b/ghost/i18n/locales/si/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "මෙම වෙබ් අඩවිය ආරාධිතයන් සඳහා පමණි, ප්\u200dරවේශ වීම සඳහා හිමිකරු අමතන්න.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/sk/portal.json b/ghost/i18n/locales/sk/portal.json index 79c1cf5192c8..36486b2eb3bb 100644 --- a/ghost/i18n/locales/sk/portal.json +++ b/ghost/i18n/locales/sk/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Táto stránka je iba pre pozvaných úžívateľov, kontaktujte vlastníka stránky.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/sl/portal.json b/ghost/i18n/locales/sl/portal.json index 4fc4285f4f3d..9121d3779904 100644 --- a/ghost/i18n/locales/sl/portal.json +++ b/ghost/i18n/locales/sl/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "To spletno mesto je dostopno samo s povabilom, obrnite se na lastnika.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/sq/portal.json b/ghost/i18n/locales/sq/portal.json index ab63e66bd55d..e42367d14468 100644 --- a/ghost/i18n/locales/sq/portal.json +++ b/ghost/i18n/locales/sq/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Kjo faqe eshte vetem me ftesa, kontaktoni zoteruesin per akses.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/sr-Cyrl/portal.json b/ghost/i18n/locales/sr-Cyrl/portal.json index 7e969ce11a77..858400556f2f 100644 --- a/ghost/i18n/locales/sr-Cyrl/portal.json +++ b/ghost/i18n/locales/sr-Cyrl/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Дошло је до грешке при обради ваше уплате. Молимо вас покушајте поново.", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Овај сајт је само на позив, контактирајте власника ради приступа.", "This site is not accepting payments at the moment.": "Овај сајт тренутно не прихвата уплате.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/sr/portal.json b/ghost/i18n/locales/sr/portal.json index e300ae10a44c..586908dcbfa1 100644 --- a/ghost/i18n/locales/sr/portal.json +++ b/ghost/i18n/locales/sr/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ovaj sajt je samo za članove, kontaktirajte vlasnika kako bi dobili pristup.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/sv/portal.json b/ghost/i18n/locales/sv/portal.json index d56cf1651f8b..55aeecc8e41a 100644 --- a/ghost/i18n/locales/sv/portal.json +++ b/ghost/i18n/locales/sv/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Det blev fel när din prenumeration skulle fortsättas, vänligen försök igen", "There was an error processing your payment. Please try again.": "Det blev fel när din betalning skulle behandlas, vänligen försök igen", "There was an error sending the email, please try again": "Det blev ett fel när e-posten skulle skickas, vänligen försök igen", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Den här sidan är endast för inbjudna, kontakta ägaren för åtkomst.", "This site is not accepting payments at the moment.": "Den här webbsidan accepterar inte betalningar för tillfället", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/sw/portal.json b/ghost/i18n/locales/sw/portal.json index 046013638c22..218f70a3df21 100644 --- a/ghost/i18n/locales/sw/portal.json +++ b/ghost/i18n/locales/sw/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Tovuti hii ni ya mialiko pekee, wasiliana na mmiliki kupata ufikiaji.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/ta/portal.json b/ghost/i18n/locales/ta/portal.json index 4b5c92473f49..b7289ef5487a 100644 --- a/ghost/i18n/locales/ta/portal.json +++ b/ghost/i18n/locales/ta/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "உங்கள் சந்தாவைத் தொடர்வதில் பிழை ஏற்பட்டது, மீண்டும் முயற்சிக்கவும்.", "There was an error processing your payment. Please try again.": "உங்கள் கட்டணத்தை செயலாக்குவதில் பிழை ஏற்பட்டது. மீண்டும் முயற்சிக்கவும்.", "There was an error sending the email, please try again": "மின்னஞ்சலை அனுப்புவதில் பிழை ஏற்பட்டது, மீண்டும் முயற்சிக்கவும்", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "இந்த தளம் அழைப்பு மட்டுமே, அணுகலுக்கு உரிமையாளரைத் தொடர்பு கொள்ளவும்.", "This site is not accepting payments at the moment.": "இந்த தளம் தற்போது கட்டணங்களை ஏற்கவில்லை.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/th/portal.json b/ghost/i18n/locales/th/portal.json index 13a3b082bff9..92f4be14b6f6 100644 --- a/ghost/i18n/locales/th/portal.json +++ b/ghost/i18n/locales/th/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "เว็บไซต์นี้สำหรับผู้ได้รับเชิญเท่านั้น โปรดติดต่อเจ้าของเพื่อเข้าถึง", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/tr/portal.json b/ghost/i18n/locales/tr/portal.json index 20047bab6603..9d3059588bd7 100644 --- a/ghost/i18n/locales/tr/portal.json +++ b/ghost/i18n/locales/tr/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Aboneliğinizi devam ettirirken bir hata oluştu, lütfen tekrar deneyin.", "There was an error processing your payment. Please try again.": "Ödemeniz işlenirken bir hata oluştu. Lütfen tekrar deneyiniz.", "There was an error sending the email, please try again": "E-posta gönderilirken bir hata oluştu, lütfen tekrar deneyin.", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Bu site sadece davetiyesi olanlar içindir, erişim için site sahibiyle iletişime geç.", "This site is not accepting payments at the moment.": "Bu site şu anda ödeme kabul etmemektedir.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/uk/portal.json b/ghost/i18n/locales/uk/portal.json index 7eae5dd5038f..4dc2eb04d087 100644 --- a/ghost/i18n/locales/uk/portal.json +++ b/ghost/i18n/locales/uk/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Під час продовження підписки сталася помилка. Спробуйте ще раз.", "There was an error processing your payment. Please try again.": "Під час обробки вашого платежу сталася помилка. Спробуйте ще раз.", "There was an error sending the email, please try again": "Під час надсилання листа сталася помилка. Повторіть спробу", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Цей сайт доступний тільки за запрошенням, звернись до власника сайта для доступу.", "This site is not accepting payments at the moment.": "Цей сайт на даний момент не приймає платежі.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/ur/portal.json b/ghost/i18n/locales/ur/portal.json index c65c245264b3..041dea35fca6 100644 --- a/ghost/i18n/locales/ur/portal.json +++ b/ghost/i18n/locales/ur/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "یہ سائٹ صرف دعوتی ہے، دستیابی کے لئے مالک سے رابطہ کریں۔", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/uz/portal.json b/ghost/i18n/locales/uz/portal.json index b08485cac70d..6e569adb5503 100644 --- a/ghost/i18n/locales/uz/portal.json +++ b/ghost/i18n/locales/uz/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Bu saytda faqat taklif qilinadi, kirish uchun egasiga murojaat qiling.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/vi/portal.json b/ghost/i18n/locales/vi/portal.json index ce097a1e3238..4c655fd91517 100644 --- a/ghost/i18n/locales/vi/portal.json +++ b/ghost/i18n/locales/vi/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "Xảy ra lỗi khi tiếp tục gói thành viên, vui lòng thử lại", "There was an error processing your payment. Please try again.": "Xảy ra lỗi khi tiến hành thanh toán. Hãy thử lại sau.", "There was an error sending the email, please try again": "Xảy ra lỗi khi gửi email, vui lòng thử lại", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Trang web này chỉ dành cho những người được mời, hãy liên hệ với chủ sở hữu để cấp quyền truy cập.", "This site is not accepting payments at the moment.": "Trang web này hiện chưa chấp nhận thanh toán.", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/zh-Hant/portal.json b/ghost/i18n/locales/zh-Hant/portal.json index 38be03f724e0..812f54e0b4d3 100644 --- a/ghost/i18n/locales/zh-Hant/portal.json +++ b/ghost/i18n/locales/zh-Hant/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "續約您的訂閱時發生錯誤,請您再試一次。", "There was an error processing your payment. Please try again.": "處理您的付款時發生錯誤,請您再試一次。", "There was an error sending the email, please try again": "寄送 email 時發生錯誤,請您再試一次。", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "此網站僅限受邀請者觀看,請聯繫網站擁有者取得存取權限。", "This site is not accepting payments at the moment.": "此網站目前無付款方式。", "This site only accepts paid members.": "", diff --git a/ghost/i18n/locales/zh/portal.json b/ghost/i18n/locales/zh/portal.json index 1db54a5c4671..016e303c01cc 100644 --- a/ghost/i18n/locales/zh/portal.json +++ b/ghost/i18n/locales/zh/portal.json @@ -165,6 +165,7 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "您的付款处理失败,请重试。", "There was an error sending the email, please try again": "", + "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "此网站仅限邀请,联系网站所有者以获取访问", "This site is not accepting payments at the moment.": "本网站目前暂不接受付款。", "This site only accepts paid members.": "", diff --git a/ghost/magic-link/lib/MagicLink.js b/ghost/magic-link/lib/MagicLink.js index b49b25a20b89..f05030a6ee0b 100644 --- a/ghost/magic-link/lib/MagicLink.js +++ b/ghost/magic-link/lib/MagicLink.js @@ -2,7 +2,8 @@ const {IncorrectUsageError, BadRequestError} = require('@tryghost/errors'); const {isEmail} = require('@tryghost/validator'); const tpl = require('@tryghost/tpl'); const messages = { - invalidEmail: 'Email is not valid' + invalidEmail: 'Email is not valid', + unsupportedEmailDomain: 'This email domain is not accepted, try again with a different email address' }; /** @@ -34,6 +35,7 @@ class MagicLink { * @param {typeof defaultGetHTML} [options.getHTML] * @param {typeof defaultGetSubject} [options.getSubject] * @param {object} [options.sentry] + * @param {object} [options.config] */ constructor(options) { if (!options || !options.transporter || !options.tokenProvider || !options.getSigninURL) { @@ -46,6 +48,7 @@ class MagicLink { this.getHTML = options.getHTML || defaultGetHTML; this.getSubject = options.getSubject || defaultGetSubject; this.sentry = options.sentry || undefined; + this.config = options.config || {}; } /** @@ -60,12 +63,19 @@ class MagicLink { */ async sendMagicLink(options) { this.sentry?.captureMessage?.(`[Magic Link] Generating magic link`, {extra: options}); - + if (!isEmail(options.email)) { throw new BadRequestError({ message: tpl(messages.invalidEmail) }); } + + if (this.isEmailDomainBlocked(options.email)) { + throw new BadRequestError({ + message: tpl(messages.unsupportedEmailDomain) + }); + } + const token = await this.tokenProvider.create(options.tokenData); const type = options.type || 'signin'; @@ -108,6 +118,24 @@ class MagicLink { const tokenData = await this.tokenProvider.validate(token); return tokenData; } + + /** + * Check if the email domain is blocked, based on the `spam.blocked_email_domains` config + * + * @param {string} email + * @returns {boolean} + */ + isEmailDomainBlocked(email) { + const emailDomain = email.split('@')[1]?.toLowerCase(); + const blockedDomains = this.config?.get('spam:blocked_email_domains'); + + // Config is not set properly: skip check + if (!blockedDomains || !Array.isArray(blockedDomains)) { + return false; + } + + return blockedDomains.includes(emailDomain); + } } /** diff --git a/ghost/magic-link/test/index.test.js b/ghost/magic-link/test/index.test.js index 22520d2671c2..b7e2de246b0c 100644 --- a/ghost/magic-link/test/index.test.js +++ b/ghost/magic-link/test/index.test.js @@ -21,6 +21,9 @@ describe('MagicLink', function () { getSubject: sandbox.stub().returns('SOMESUBJECT'), transporter: { sendMail: sandbox.stub().resolves() + }, + config: { + get: sandbox.stub().resolves() } }; const service = new MagicLink(options); @@ -53,6 +56,9 @@ describe('MagicLink', function () { getSubject: sandbox.stub().returns('SOMESUBJECT'), transporter: { sendMail: sandbox.stub().resolves() + }, + config: { + get: sandbox.stub().resolves() } }; const service = new MagicLink(options); @@ -85,6 +91,48 @@ describe('MagicLink', function () { assert.equal(options.transporter.sendMail.firstCall.args[0].text, options.getText.firstCall.returnValue); assert.equal(options.transporter.sendMail.firstCall.args[0].html, options.getHTML.firstCall.returnValue); }); + + it('Blocks signups from blocked email domains', async function () { + const options = { + tokenProvider: new MagicLink.JWTTokenProvider(secret), + getSigninURL: sandbox.stub().returns('FAKEURL'), + getText: sandbox.stub().returns('SOMETEXT'), + getHTML: sandbox.stub().returns('SOMEHTML'), + getSubject: sandbox.stub().returns('SOMESUBJECT'), + transporter: { + sendMail: sandbox.stub().resolves() + }, + config: { + get: sandbox.stub().withArgs('spam:blocked_email_domains').returns(['blocked-domain.com']) + } + }; + const service = new MagicLink(options); + + const blockedArgs = { + email: 'test@blocked-domain.com', + tokenData: { + id: '420' + } + }; + + await assert.rejects( + () => service.sendMagicLink(blockedArgs), + { + name: 'BadRequestError', + message: 'This email domain is not accepted, try again with a different email address' + } + ); + + // Verify non-blocked domain is allowed + const allowedArgs = { + email: 'test@allowed-domain.com', + tokenData: { + id: '420' + } + }; + + await assert.doesNotReject(() => service.sendMagicLink(allowedArgs)); + }); }); describe('#getDataFromToken', function () { @@ -96,6 +144,9 @@ describe('MagicLink', function () { getHTML: sandbox.stub().returns('SOMEHTML'), transporter: { sendMail: sandbox.stub().resolves() + }, + config: { + get: sandbox.stub().resolves() } }; const service = new MagicLink(options); diff --git a/ghost/members-api/lib/members-api.js b/ghost/members-api/lib/members-api.js index 6bacee6d92a2..660bc6cdca71 100644 --- a/ghost/members-api/lib/members-api.js +++ b/ghost/members-api/lib/members-api.js @@ -73,7 +73,8 @@ module.exports = function MembersAPI({ emailSuppressionList, settingsCache, sentry, - settingsHelpers + settingsHelpers, + config }) { const tokenService = new TokenService({ privateKey, @@ -158,7 +159,8 @@ module.exports = function MembersAPI({ getText, getHTML, getSubject, - sentry + sentry, + config }); const paymentsService = new PaymentsService({ From ff4545939c98fddb36dfb7144f201a198325014b Mon Sep 17 00:00:00 2001 From: Ghost CI <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:07:50 +0000 Subject: [PATCH 02/27] v5.107.1 --- ghost/admin/package.json | 2 +- ghost/core/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 52061fc8d4b8..e5ea26455837 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -1,6 +1,6 @@ { "name": "ghost-admin", - "version": "5.107.0", + "version": "5.107.1", "description": "Ember.js admin client for Ghost", "author": "Ghost Foundation", "homepage": "http://ghost.org", diff --git a/ghost/core/package.json b/ghost/core/package.json index 76e0d79632cc..f5b948a78863 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -1,6 +1,6 @@ { "name": "ghost", - "version": "5.107.0", + "version": "5.107.1", "description": "The professional publishing platform", "author": "Ghost Foundation", "homepage": "https://ghost.org", From c1d7a46599b770743f7307fdec967b0a2118b97e Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Mon, 20 Jan 2025 15:08:37 +0000 Subject: [PATCH 03/27] Added truncation and "Show more" button for long notes ref https://linear.app/ghost/issue/AP-618/show-only-excerpts-for-very-long-notes-in-the-feed - Notes can be pretty long and we used to show them in their entirety, so they could take up a large chunk of the viewport. Now we're limiting the displayed text in notes to 10 lines, and we show a "Show more" button to indicate there is more content. --- .../src/components/feed/FeedItem.tsx | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/apps/admin-x-activitypub/src/components/feed/FeedItem.tsx b/apps/admin-x-activitypub/src/components/feed/FeedItem.tsx index c857f16fe359..6a2ca1fdc56d 100644 --- a/apps/admin-x-activitypub/src/components/feed/FeedItem.tsx +++ b/apps/admin-x-activitypub/src/components/feed/FeedItem.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub'; import {Button, Heading, Icon, Menu, MenuItem, showToast} from '@tryghost/admin-x-design-system'; @@ -166,6 +166,16 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment const [, setIsCopied] = useState(false); + const contentRef = useRef(null); + const [isTruncated, setIsTruncated] = useState(false); + + useEffect(() => { + const element = contentRef.current; + if (element) { + setIsTruncated(element.scrollHeight > element.clientHeight); + } + }, [object.content]); + const onLikeClick = () => { // Do API req or smth // Don't need to know about setting timeouts or anything like that @@ -264,10 +274,23 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment
{(object.type === 'Article') && renderFeedAttachment(object, layout)} {object.name && {object.name}} - {(object.preview && object.type === 'Article') ?
{object.preview.content}
:
} + {(object.preview && object.type === 'Article') ? ( +
{object.preview.content}
+ ) : ( +
+
+ {isTruncated && ( + + )} +
+ )} {(object.type === 'Note') && renderFeedAttachment(object, layout)} {(object.type === 'Article') && )} + {renderFeedAttachment(object, layout)}
- )} - {(object.type === 'Note') && renderFeedAttachment(object, layout)} - {(object.type === 'Article') && + + + + + + + Share + + This is a dialog, lets see how it works. + + + + diff --git a/apps/posts/src/routes.ts b/apps/posts/src/routes.ts new file mode 100644 index 000000000000..8e1f20afd829 --- /dev/null +++ b/apps/posts/src/routes.ts @@ -0,0 +1,30 @@ +import Newsletter from './views/post-analytics/components/Newsletter'; +import Overview from './views/post-analytics/components/Overview'; +import PostAnalytics from './views/post-analytics/PostAnalytics'; +import {createHashRouter} from 'react-router'; + +export const BASE_PATH = '/posts-x'; +export const ANALYTICS = `${BASE_PATH}/analytics`; + +const postAnalyticsRoutes = [ + { + path: `${BASE_PATH}/analytics/:postId`, + Component: PostAnalytics, + children: [ + { + path: '', + Component: Overview + }, + { + path: 'overview', + Component: Overview + }, + { + path: 'newsletter', + Component: Newsletter + } + ] + } +]; + +export const router = createHashRouter(postAnalyticsRoutes); diff --git a/apps/posts/src/views/post-analytics/PostAnalytics.tsx b/apps/posts/src/views/post-analytics/PostAnalytics.tsx index 90cb9a8c64ea..ebb78a58f63f 100644 --- a/apps/posts/src/views/post-analytics/PostAnalytics.tsx +++ b/apps/posts/src/views/post-analytics/PostAnalytics.tsx @@ -1,26 +1,44 @@ import Header from '../../components/Header'; -import Newsletter from './components/Newsletter'; -import Overview from './components/Overview'; -import {LucideIcon, Page, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade'; +import {ANALYTICS} from '../../routes'; +import {Outlet, useLocation, useNavigate, useParams} from 'react-router'; +import {Page, Tabs, TabsList, TabsTrigger} from '@tryghost/shade'; interface postAnalyticsProps {}; const PostAnalytics: React.FC = () => { + const navigate = useNavigate(); + const {postId} = useParams(); + const location = useLocation(); + + let currentTab = location.pathname.split('/').pop(); + if (currentTab === postId || !currentTab) { + currentTab = 'overview'; + } + + const handleTabChange = (value: string) => { + if (value === 'overview') { + navigate(`${ANALYTICS}/${postId}`); + } else { + navigate(`${ANALYTICS}/${postId}/${value}`); + } + }; + return (
- + - Overview - Newsletter - Web + Overview + Newsletter - - - - - - +
+ +
); diff --git a/apps/posts/src/views/post-analytics/components/Newsletter.tsx b/apps/posts/src/views/post-analytics/components/Newsletter.tsx index 8982064470ed..4181824d121f 100644 --- a/apps/posts/src/views/post-analytics/components/Newsletter.tsx +++ b/apps/posts/src/views/post-analytics/components/Newsletter.tsx @@ -1,7 +1,7 @@ import OpenedList from './newsletter/OpenedList'; import React from 'react'; import SentList from './newsletter/SentList'; -import {Badge} from '@tryghost/shade'; +import {Badge, Card, CardContent} from '@tryghost/shade'; import {StatsTabItem, StatsTabTitle, StatsTabValue, StatsTabs, StatsTabsGroup} from './StatsTabs'; interface newsletterProps {}; @@ -13,7 +13,7 @@ const Newsletter: React.FC = () => { key: 'sent', title: 'Sent', value: '1,697', - badge: '', + badge: '100%', content: }, { @@ -73,10 +73,12 @@ const Newsletter: React.FC = () => { }; return ( -
-
- -
+
+ + + + +
{tabs.map(group => ( diff --git a/apps/posts/src/views/post-analytics/components/StatsTabs.tsx b/apps/posts/src/views/post-analytics/components/StatsTabs.tsx index 3a79d034bcd4..22e49f891592 100644 --- a/apps/posts/src/views/post-analytics/components/StatsTabs.tsx +++ b/apps/posts/src/views/post-analytics/components/StatsTabs.tsx @@ -6,7 +6,7 @@ interface statsTabsProps extends React.HTMLAttributes {} const StatsTabs: React.FC = ({className, ...props}) => { - return
; + return
; }; interface statsTabsGroupProps diff --git a/apps/posts/src/views/post-analytics/components/overview/NewsletterPerformance.tsx b/apps/posts/src/views/post-analytics/components/overview/NewsletterPerformance.tsx index db7a7f94cb3c..2c28bb02e7cc 100644 --- a/apps/posts/src/views/post-analytics/components/overview/NewsletterPerformance.tsx +++ b/apps/posts/src/views/post-analytics/components/overview/NewsletterPerformance.tsx @@ -35,7 +35,7 @@ const NewsletterPerformance: React.FC = (props) => {
Sent - 1,697 + 1,697 100% diff --git a/apps/shade/src/components/ui/dialog.tsx b/apps/shade/src/components/ui/dialog.tsx new file mode 100644 index 000000000000..176b9b0bc972 --- /dev/null +++ b/apps/shade/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import {X} from 'lucide-react'; + +import {cn} from '@/lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, children, ...props}, ref) => ( + +
+ + + {children} + + + Close + + +
+
+)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({className, ...props}, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription +}; diff --git a/apps/shade/src/components/ui/tabs.tsx b/apps/shade/src/components/ui/tabs.tsx index a69b8bdfe92f..ab17bf57f3e2 100644 --- a/apps/shade/src/components/ui/tabs.tsx +++ b/apps/shade/src/components/ui/tabs.tsx @@ -76,7 +76,7 @@ const tabsTriggerVariants = cva( variant: { segmented: 'h-7 rounded-md text-sm font-medium data-[state=active]:shadow-md', button: 'h-[34px] gap-1.5 rounded-md border border-input py-2 text-sm font-medium hover:bg-muted/50 data-[state=active]:bg-muted/70 data-[state=active]:font-semibold', - underline: 'relative h-[34px] px-0 text-md font-medium text-gray-700 data-[state=active]:font-semibold data-[state=active]:text-black data-[state=active]:after:absolute data-[state=active]:after:inset-x-0 data-[state=active]:after:bottom-[-5px] data-[state=active]:after:h-px data-[state=active]:after:bg-black data-[state=active]:after:content-[""]' + underline: 'relative h-[34px] px-0 text-md font-medium text-gray-700 data-[state=active]:font-semibold data-[state=active]:text-black data-[state=active]:after:absolute data-[state=active]:after:inset-x-0 data-[state=active]:after:bottom-[-5px] data-[state=active]:after:h-0.5 data-[state=active]:after:bg-black data-[state=active]:after:content-[""]' } }, defaultVariants: { diff --git a/apps/shade/src/index.ts b/apps/shade/src/index.ts index 7574e40f260b..dab7abfff2a9 100644 --- a/apps/shade/src/index.ts +++ b/apps/shade/src/index.ts @@ -5,6 +5,7 @@ export * from './components/ui/breadcrumb'; export * from './components/ui/button'; export * from './components/ui/card'; export * from './components/ui/chart'; +export * from './components/ui/dialog'; export * from './components/ui/dropdown-menu'; export * from './components/ui/input'; export * from './components/ui/separator'; diff --git a/ghost/admin/app/components/gh-nav-menu/main.hbs b/ghost/admin/app/components/gh-nav-menu/main.hbs index b1d75ec98368..1a5392416fa8 100644 --- a/ghost/admin/app/components/gh-nav-menu/main.hbs +++ b/ghost/admin/app/components/gh-nav-menu/main.hbs @@ -164,7 +164,7 @@ {{/if}} {{#if (feature "postsX")}}
  • - {{svg-jar "chart"}}Post analytics + {{svg-jar "chart"}}Post analytics
  • {{/if}} diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js index 5fa7d260a088..5c0a1352a1a6 100644 --- a/ghost/admin/app/router.js +++ b/ghost/admin/app/router.js @@ -54,6 +54,9 @@ Router.map(function () { this.route('posts-x', function () { this.route('posts-x', {path: '/*sub'}); + this.route('analytics', function () { + this.route('123'); + }); }); this.route('settings-x', {path: '/settings'}, function () { diff --git a/yarn.lock b/yarn.lock index 882f25dc332d..5ba170a3f824 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8276,6 +8276,11 @@ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q== +"@types/cookie@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" + integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== + "@types/cookiejar@^2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" @@ -13562,6 +13567,11 @@ cookie@0.7.2: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== +cookie@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-1.0.2.tgz#27360701532116bd3f1f9416929d176afe1e4610" + integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== + cookie@~0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" @@ -27580,6 +27590,16 @@ react-remove-scroll@^2.6.1: use-callback-ref "^1.3.3" use-sidecar "^1.1.2" +react-router@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-7.1.3.tgz#6c15c28838b799cb3058943e8e8015dbd6c16c7b" + integrity sha512-EezYymLY6Guk/zLQ2vRA8WvdUhWFEj5fcE3RfWihhxXBW7+cd1LsIiA3lmx+KCmneAGQuyBv820o44L2+TtkSA== + dependencies: + "@types/cookie" "^0.6.0" + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + turbo-stream "2.4.0" + react-select@5.8.2: version "5.8.2" resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.8.2.tgz#0d7ccb1895d61aafcd090fbf65aa9e506225a854" @@ -28871,6 +28891,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-cookie-parser@^2.6.0: + version "2.7.1" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz#3016f150072202dfbe90fadee053573cc89d2943" + integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== + set-function-length@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" @@ -31071,6 +31096,11 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +turbo-stream@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.4.0.tgz#1e4fca6725e90fa14ac4adb782f2d3759a5695f0" + integrity sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" From 713e75838ae7db4e4b8865946703e78684680419 Mon Sep 17 00:00:00 2001 From: Chris Raible Date: Tue, 21 Jan 2025 11:22:45 -0800 Subject: [PATCH 11/27] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20newsletters=20not?= =?UTF-8?q?=20rendering=20in=20Portal=20Email=20Preferences=20(#22037)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ref https://linear.app/ghost/issue/ONC-723/support-escalation-re-fwd-email-preferences - On sites where the Default recipients setting was set to anything other than "Whoever has access to the post", the list of newsletters and the toggle to subscribe/unsubscribe would not be rendered on the Portal "Email Preferences" page. - The bug was introduced in v5.106.0, and intended to hide the newsletter list if Newsletter sending were disabled completely, but there was bug in the logic - This commit has a breaking test to prevent this in the future, and fixes the logic to only hide the newsletter list if `editor_default_email_recipients` is explicitly set to 'disabled'. --- .../components/pages/AccountEmailPage.test.js | 24 +++++++++++++++++++ apps/portal/src/utils/helpers.js | 2 +- apps/portal/src/utils/helpers.test.js | 19 ++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/apps/portal/src/components/pages/AccountEmailPage.test.js b/apps/portal/src/components/pages/AccountEmailPage.test.js index c5cb88f762f7..1c47bc1713ed 100644 --- a/apps/portal/src/components/pages/AccountEmailPage.test.js +++ b/apps/portal/src/components/pages/AccountEmailPage.test.js @@ -134,4 +134,28 @@ describe('Account Email Page', () => { expect(unsubscribeBtns).toHaveLength(1); expect(unsubscribeBtns[0].textContent).toContain('Get notified when someone replies to your comment'); }); + + test('newsletters are visible when editor default email recipients is set to visibility', async () => { + const newsletterData = getNewslettersData({numOfNewsletters: 2}); + const siteData = getSiteData({ + newsletters: newsletterData, + editorDefaultEmailRecipients: 'visibility', + member: getMemberData({newsletters: newsletterData}) + }); + const {getAllByTestId} = setup({site: siteData}); + const unsubscribeBtns = getAllByTestId(`toggle-wrapper`); + expect(unsubscribeBtns).toHaveLength(3); + }); + + test('newsletters are visible when editor default email recipients is set to filter', async () => { + const newsletterData = getNewslettersData({numOfNewsletters: 2}); + const siteData = getSiteData({ + newsletters: newsletterData, + editorDefaultEmailRecipients: 'filter', + member: getMemberData({newsletters: newsletterData}) + }); + const {getAllByTestId} = setup({site: siteData}); + const unsubscribeBtns = getAllByTestId(`toggle-wrapper`); + expect(unsubscribeBtns).toHaveLength(3); + }); }); diff --git a/apps/portal/src/utils/helpers.js b/apps/portal/src/utils/helpers.js index 27d1e0bf15ff..ce70d3b9a3d7 100644 --- a/apps/portal/src/utils/helpers.js +++ b/apps/portal/src/utils/helpers.js @@ -87,7 +87,7 @@ export function getNewsletterFromUuid({site, uuid}) { } export function hasNewsletterSendingEnabled({site}) { - return site?.editor_default_email_recipients === 'visibility'; + return site?.editor_default_email_recipients !== 'disabled'; } export function allowCompMemberUpgrade({member}) { diff --git a/apps/portal/src/utils/helpers.test.js b/apps/portal/src/utils/helpers.test.js index b58af2868eb0..a21301b59f79 100644 --- a/apps/portal/src/utils/helpers.test.js +++ b/apps/portal/src/utils/helpers.test.js @@ -1,4 +1,4 @@ -import {hasAvailablePrices, getAllProductsForSite, getAvailableProducts, getCurrencySymbol, getFreeProduct, getMemberName, getMemberSubscription, getPriceFromSubscription, getPriceIdFromPageQuery, getSupportAddress, getDefaultNewsletterSender, getUrlHistory, hasMultipleProducts, isActiveOffer, isInviteOnly, isPaidMember, isPaidMembersOnly, isSameCurrency, transformApiTiersData, isSigninAllowed, isSignupAllowed, getCompExpiry, isInThePast} from './helpers'; +import {hasAvailablePrices, getAllProductsForSite, getAvailableProducts, getCurrencySymbol, getFreeProduct, getMemberName, getMemberSubscription, getPriceFromSubscription, getPriceIdFromPageQuery, getSupportAddress, getDefaultNewsletterSender, getUrlHistory, hasMultipleProducts, isActiveOffer, isInviteOnly, isPaidMember, isPaidMembersOnly, isSameCurrency, transformApiTiersData, isSigninAllowed, isSignupAllowed, getCompExpiry, isInThePast, hasNewsletterSendingEnabled} from './helpers'; import * as Fixtures from './fixtures-generator'; import {site as FixturesSite, member as FixtureMember, offer as FixtureOffer, transformTierFixture as TransformFixtureTiers} from '../utils/test-fixtures'; import {isComplimentaryMember} from '../utils/helpers'; @@ -539,4 +539,21 @@ describe('Helpers - ', () => { expect(isInThePast(futureDate)).toEqual(false); }); }); + + describe('hasNewsletterSendingEnabled', () => { + test('returns true when editor default email recipients is set to visibility', () => { + const site = {editor_default_email_recipients: 'visibility'}; + expect(hasNewsletterSendingEnabled({site})).toBe(true); + }); + + test('returns false when editor default email recipients is set to disabled', () => { + const site = {editor_default_email_recipients: 'disabled'}; + expect(hasNewsletterSendingEnabled({site})).toBe(false); + }); + + test('returns true when editor default email recipients is set to filter', () => { + const site = {editor_default_email_recipients: 'filter'}; + expect(hasNewsletterSendingEnabled({site})).toBe(true); + }); + }); }); From 669da1cfb1caed2d5367ff8e4dabbc050a342f5a Mon Sep 17 00:00:00 2001 From: Chris Raible Date: Tue, 21 Jan 2025 13:04:23 -0800 Subject: [PATCH 12/27] Shipped portal@2.48.1 (#22039) Patch update including this bug fix: https://github.com/TryGhost/Ghost/commit/713e75838ae7db4e4b8865946703e78684680419 --- apps/portal/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/portal/package.json b/apps/portal/package.json index 0ebc1a99eedf..2e3d5040e8a1 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/portal", - "version": "2.48.0", + "version": "2.48.1", "license": "MIT", "repository": { "type": "git", From 3ca419bcbce4acf27354f269a9b3b6b311666089 Mon Sep 17 00:00:00 2001 From: Sag Date: Wed, 22 Jan 2025 11:40:22 +0700 Subject: [PATCH 13/27] Improved error message when email provider is blocked (#22040) ref https://linear.app/ghost/issue/ONC-721 ref https://linear.app/ghost/issue/PRO-1349 - also added the rate limit error message into the translate-able strings in Portal --- apps/portal/src/utils/errors.js | 3 ++- .../e2e-api/members/__snapshots__/send-magic-link.test.js.snap | 2 +- ghost/core/test/e2e-api/members/send-magic-link.test.js | 2 +- ghost/i18n/locales/af/portal.json | 3 ++- ghost/i18n/locales/ar/portal.json | 3 ++- ghost/i18n/locales/bg/portal.json | 3 ++- ghost/i18n/locales/bn/portal.json | 3 ++- ghost/i18n/locales/bs/portal.json | 3 ++- ghost/i18n/locales/ca/portal.json | 3 ++- ghost/i18n/locales/context.json | 3 ++- ghost/i18n/locales/cs/portal.json | 3 ++- ghost/i18n/locales/da/portal.json | 3 ++- ghost/i18n/locales/de-CH/portal.json | 3 ++- ghost/i18n/locales/de/portal.json | 3 ++- ghost/i18n/locales/el/portal.json | 3 ++- ghost/i18n/locales/en/portal.json | 3 ++- ghost/i18n/locales/eo/portal.json | 3 ++- ghost/i18n/locales/es/portal.json | 3 ++- ghost/i18n/locales/et/portal.json | 3 ++- ghost/i18n/locales/fa/portal.json | 3 ++- ghost/i18n/locales/fi/portal.json | 3 ++- ghost/i18n/locales/fr/portal.json | 3 ++- ghost/i18n/locales/gd/portal.json | 3 ++- ghost/i18n/locales/he/portal.json | 3 ++- ghost/i18n/locales/hi/portal.json | 3 ++- ghost/i18n/locales/hr/portal.json | 3 ++- ghost/i18n/locales/hu/portal.json | 3 ++- ghost/i18n/locales/id/portal.json | 3 ++- ghost/i18n/locales/is/portal.json | 3 ++- ghost/i18n/locales/it/portal.json | 3 ++- ghost/i18n/locales/ja/portal.json | 3 ++- ghost/i18n/locales/ko/portal.json | 3 ++- ghost/i18n/locales/kz/portal.json | 3 ++- ghost/i18n/locales/lt/portal.json | 3 ++- ghost/i18n/locales/lv/portal.json | 3 ++- ghost/i18n/locales/mk/portal.json | 3 ++- ghost/i18n/locales/mn/portal.json | 3 ++- ghost/i18n/locales/ms/portal.json | 3 ++- ghost/i18n/locales/ne/portal.json | 3 ++- ghost/i18n/locales/nl/portal.json | 3 ++- ghost/i18n/locales/nn/portal.json | 3 ++- ghost/i18n/locales/no/portal.json | 3 ++- ghost/i18n/locales/pl/portal.json | 3 ++- ghost/i18n/locales/pt-BR/portal.json | 3 ++- ghost/i18n/locales/pt/portal.json | 3 ++- ghost/i18n/locales/ro/portal.json | 3 ++- ghost/i18n/locales/ru/portal.json | 3 ++- ghost/i18n/locales/si/portal.json | 3 ++- ghost/i18n/locales/sk/portal.json | 3 ++- ghost/i18n/locales/sl/portal.json | 3 ++- ghost/i18n/locales/sq/portal.json | 3 ++- ghost/i18n/locales/sr-Cyrl/portal.json | 3 ++- ghost/i18n/locales/sr/portal.json | 3 ++- ghost/i18n/locales/sv/portal.json | 3 ++- ghost/i18n/locales/sw/portal.json | 3 ++- ghost/i18n/locales/ta/portal.json | 3 ++- ghost/i18n/locales/th/portal.json | 3 ++- ghost/i18n/locales/tr/portal.json | 3 ++- ghost/i18n/locales/uk/portal.json | 3 ++- ghost/i18n/locales/ur/portal.json | 3 ++- ghost/i18n/locales/uz/portal.json | 3 ++- ghost/i18n/locales/vi/portal.json | 3 ++- ghost/i18n/locales/zh-Hant/portal.json | 3 ++- ghost/i18n/locales/zh/portal.json | 3 ++- ghost/magic-link/lib/MagicLink.js | 2 +- ghost/magic-link/test/index.test.js | 2 +- 66 files changed, 128 insertions(+), 66 deletions(-) diff --git a/apps/portal/src/utils/errors.js b/apps/portal/src/utils/errors.js index 93034e1cc981..a3ad55d08819 100644 --- a/apps/portal/src/utils/errors.js +++ b/apps/portal/src/utils/errors.js @@ -59,7 +59,8 @@ export function chooseBestErrorMessage(error, alreadyTranslatedDefaultMessage, t t('Too many different sign-in attempts, try again in {{number}} days'); t('Failed to send magic link email'); t('This site only accepts paid members.'); - t('This email domain is not accepted, try again with a different email address'); + t('Signups from this email provider are not allowed'); + t('Too many sign-up attempts, try again later'); } }; diff --git a/ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap b/ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap index 1305fc1af3d1..2a27b32c1a8a 100644 --- a/ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap +++ b/ghost/core/test/e2e-api/members/__snapshots__/send-magic-link.test.js.snap @@ -118,7 +118,7 @@ Object { "ghostErrorCode": null, "help": null, "id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - "message": "This email domain is not accepted, try again with a different email address", + "message": "Signups from this email provider are not allowed", "property": null, "type": "BadRequestError", }, diff --git a/ghost/core/test/e2e-api/members/send-magic-link.test.js b/ghost/core/test/e2e-api/members/send-magic-link.test.js index 88eefe3869d0..f10faaf718f9 100644 --- a/ghost/core/test/e2e-api/members/send-magic-link.test.js +++ b/ghost/core/test/e2e-api/members/send-magic-link.test.js @@ -301,7 +301,7 @@ describe('sendMagicLink', function () { .matchBodySnapshot({ errors: [{ id: anyErrorId, - message: 'This email domain is not accepted, try again with a different email address' + message: 'Signups from this email provider are not allowed' }] }); }); diff --git a/ghost/i18n/locales/af/portal.json b/ghost/i18n/locales/af/portal.json index 90cc19596033..253ab1b4141d 100644 --- a/ghost/i18n/locales/af/portal.json +++ b/ghost/i18n/locales/af/portal.json @@ -138,6 +138,7 @@ "Sign out": "Teken uit", "Sign up": "Registreer", "Signup error: Invalid link": "Aanmelding fout: Ongeldige skakel", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Jammer, dit het nie gewerk nie.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Hierdie webwerf is slegs op uitnodiging, kontak die eienaar vir toegang.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Probeer gratis vir {{amount}} dae, dan {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Ontsluit toegang tot alle nuusbriewe deur 'n betaalde intekenaar te word.", diff --git a/ghost/i18n/locales/ar/portal.json b/ghost/i18n/locales/ar/portal.json index e781b56499e0..c4108b6d24aa 100644 --- a/ghost/i18n/locales/ar/portal.json +++ b/ghost/i18n/locales/ar/portal.json @@ -138,6 +138,7 @@ "Sign out": "تسجيل الخروج", "Sign up": "إنشاء حساب", "Signup error: Invalid link": "خطأ في التسجيل: الرابط غير صالح", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": ".حدث خطأ ما، يرجى المحاولة مرة أخرى لاحقًا", "Sorry, no recommendations are available right now.": ".عذرًا، لا توجد توصيات متاحة حاليًا", "Sorry, that didn’t work.": ".عذرًا، لم ينجح ذلك", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": ".حدث خطأ أثناء الاشتراك، يرجى المحاولة مرة أخرى", "There was an error processing your payment. Please try again.": ".حدث خطأ أثناء معالجة دفعك، يرجى المحاولة مرة أخرى", "There was an error sending the email, please try again": ".حدث خطأ أثناء إرسال البريد الاكتروني، يرجى المحاولة مرة أخرى", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "هذا الموقع للمشتركين فقط، تواصل مع ادارة الموقع للحصول على اشتراك.", "This site is not accepting payments at the moment.": "هذا الموقع لا يقبل المدفوعات في الوقت الحالي", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": ".أيام{{number}} محاولات تسجيل الدخول متعددة جدًا، حاول مرة أخري بعد ", "Too many different sign-in attempts, try again in {{number}} hours": ".ساعة {{number}} محاولات تسجيل الدخول متعددة جدًا، حاول مرة أخري بعد ", "Too many different sign-in attempts, try again in {{number}} minutes": ".دقيقة {{number}} محاولات تسجيل الدخول متعددة جدًا، حاول مرة أخري بعد ", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": " .{{originalPrice}} يوما، ثم {{amount}} جرب مجانًا لمدة", "Unable to initiate checkout session": "غير قادر على بدء جلسة الدفع", "Unlock access to all newsletters by becoming a paid subscriber.": ".افتح الوصول إلي جميع النشرات الإخبارية من خلال الاشتراك المدفوع", diff --git a/ghost/i18n/locales/bg/portal.json b/ghost/i18n/locales/bg/portal.json index 3960c1dd0f2a..168eef1693a6 100644 --- a/ghost/i18n/locales/bg/portal.json +++ b/ghost/i18n/locales/bg/portal.json @@ -138,6 +138,7 @@ "Sign out": "Изход", "Sign up": "Регистриране", "Signup error: Invalid link": "Грешка при влизане: Невалиден линк", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Нещо се обърка, опитайте отново.", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Жалко, така не става.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Възникна грешка при продължаването на абонамента ви, опитайте отново.", "There was an error processing your payment. Please try again.": "Възникна грешка при обработката на вашето плащане. Моля, опитайте отново.", "There was an error sending the email, please try again": "Възникна грешка при изпращане на имейл, опитайте отново", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Сайтът е само с покани. Свържете се със собственика за да получите достъп.", "This site is not accepting payments at the moment.": "В момента сайтът не приема плащания.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Твърде много различни опити за влизане, опитайте отново след {{number}} дни", "Too many different sign-in attempts, try again in {{number}} hours": "Твърде много различни опити за влизане, опитайте отново след {{number}} часа", "Too many different sign-in attempts, try again in {{number}} minutes": "Твърде много различни опити за влизане, опитайте отново след {{number}} минути", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Тествайте безплатно за {{amount}} дни, след това {{originalPrice}}.", "Unable to initiate checkout session": "Невъзможност за започване на сесия за плащане", "Unlock access to all newsletters by becoming a paid subscriber.": "Отключете достъпа до всички бюлетини, като станете платен абонат.", diff --git a/ghost/i18n/locales/bn/portal.json b/ghost/i18n/locales/bn/portal.json index a89cfc896826..2e19743fceb8 100644 --- a/ghost/i18n/locales/bn/portal.json +++ b/ghost/i18n/locales/bn/portal.json @@ -138,6 +138,7 @@ "Sign out": "সাইন আউট করুন", "Sign up": "সাইন আপ করুন", "Signup error: Invalid link": "সাইন আপ ত্রুটি: অবৈধ লিঙ্ক", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "দুঃখিত, এটি কাজ করেনি।", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "এই সাইটটি কেবল আমন্ত্রণের মাধ্যমে, প্রবেশাধিকার পেতে মালিকের সাথে যোগাযোগ করুন।", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}} দিনের জন্য ফ্রি চেষ্টা করুন, তারপর {{originalPrice}}।", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "পেইড সাবস্ক্রাইবার হয়ে সমস্ত নিউজলেটারে প্রবেশাধিকার আনলক করুন।", diff --git a/ghost/i18n/locales/bs/portal.json b/ghost/i18n/locales/bs/portal.json index 0d2da7661caa..f6eb714e94e4 100644 --- a/ghost/i18n/locales/bs/portal.json +++ b/ghost/i18n/locales/bs/portal.json @@ -138,6 +138,7 @@ "Sign out": "Odjavi se", "Sign up": "Registracija", "Signup error: Invalid link": "Greška pri prijavi: Neispravan link", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Žao nam je, to nije uspjelo.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ova je stranica samo na poziv, kontaktiraj vlasnika za pristup.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Isprobaj besplatno {{amount}} dana, a zatim plati {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Otključaj pristup svim newsletterima tako što ćete postati plaćeni član.", diff --git a/ghost/i18n/locales/ca/portal.json b/ghost/i18n/locales/ca/portal.json index 16f7031facd2..9e8ba84060fa 100644 --- a/ghost/i18n/locales/ca/portal.json +++ b/ghost/i18n/locales/ca/portal.json @@ -138,6 +138,7 @@ "Sign out": "Finalitza la sessió", "Sign up": "Registrar-se", "Signup error: Invalid link": "Error de registre: Enllaç no vàlid", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Em sap greu, pero no ha funcionat.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Aquest llog és només per invitació, contacta amb el propietari per obtenir accés.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prova gratis durant {{amount}} dies i després {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Desbloqueja l'accés a tots els butlletins de notícies fent-te un subscriptor de pagament.", diff --git a/ghost/i18n/locales/context.json b/ghost/i18n/locales/context.json index 9d845830c9cf..efe8d12d2fbf 100644 --- a/ghost/i18n/locales/context.json +++ b/ghost/i18n/locales/context.json @@ -202,6 +202,7 @@ "Sign up": "A button to sign up", "Sign up now": "Button text to sign up in order to post a comment", "Signup error: Invalid link": "Notification text when an invalid / expired signup link is used", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Something went wrong, please try again.": "Error message when subscribing to a newsletter fails in the signup form embed", "Sorry, no recommendations are available right now.": "", @@ -242,7 +243,6 @@ "This comment has been hidden.": "Text for a comment thas was hidden", "This comment has been removed.": "Text for a comment thas was removed", "This email address will not be used.": "This is in the footer of signup verification emails, and comes right after 'If you did not make this request, you can simply delete this message.'", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "A message on the member login screen indicating that a site is not-open to public signups", "This site is not accepting payments at the moment.": "An error message shown when a tips or donations link is opened but the site has donations disabled", "This site only accepts paid members.": "", @@ -254,6 +254,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "A label for an offer with a free trial", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "A message to encourage members to upgrade to a paid subscription", diff --git a/ghost/i18n/locales/cs/portal.json b/ghost/i18n/locales/cs/portal.json index f4a806b16078..eef5865cdfe3 100644 --- a/ghost/i18n/locales/cs/portal.json +++ b/ghost/i18n/locales/cs/portal.json @@ -138,6 +138,7 @@ "Sign out": "Odhlásit se", "Sign up": "Registrovat se", "Signup error: Invalid link": "Chyba registrace: Neplatný odkaz", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Něco se pokazilo, zkuste to prosím později.", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Omlouváme se, to nefungovalo.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Při zpracování vaší platby došlo k chybě. Zkuste to prosím znovu.", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Tento web je pouze pro pozvané, kontaktujte provozovatele pro přístup.", "This site is not accepting payments at the moment.": "Tento web momentálně nepřijímá platby.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Vyzkoušejte zdarma na {{amount}} dní, poté {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Odemkněte přístup ke všem newsletterům tím, že se stanete placeným odběratelem.", diff --git a/ghost/i18n/locales/da/portal.json b/ghost/i18n/locales/da/portal.json index 03c1eb8c7880..6c0dbe0ac541 100644 --- a/ghost/i18n/locales/da/portal.json +++ b/ghost/i18n/locales/da/portal.json @@ -138,6 +138,7 @@ "Sign out": "Log ud", "Sign up": "Bliv medlem", "Signup error: Invalid link": "Tilmeldingsfejl: Ugyldigt link", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Noget gik galt, prøv venligst igen senere.", "Sorry, no recommendations are available right now.": "Beklager, der er ingen anbefalinger tilgængelige lige nu.", "Sorry, that didn’t work.": "Beklager, det virkede ikke.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Der opstod en fejl under forlængelsen af dit abonnement, prøv venligst igen.", "There was an error processing your payment. Please try again.": "Der opstod en fejl under behandlingen af din betaling. Prøv venligst igen.", "There was an error sending the email, please try again": "Der opstod en fejl under afsendelse af e-mailen, prøv venligst igen.", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Denne sider kræver at du skal være inviteret. Kontakt ejeres for at få adgang.", "This site is not accepting payments at the moment.": "Denne side accepterer ikke betalinger i øjeblikket.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "For mange forskellige log-in forsøg, prøv igen om {{number}} dage.", "Too many different sign-in attempts, try again in {{number}} hours": "For mange forskellige log-in forsøg, prøv igen om {{number}} timer.", "Too many different sign-in attempts, try again in {{number}} minutes": "For mange forskellige log-in forsøg, prøv igen om {{number}} minutter.", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prøv gratis i {{amount}} dage, derefter {{original Price}}.", "Unable to initiate checkout session": "Kan ikke starte betalings-session", "Unlock access to all newsletters by becoming a paid subscriber.": "Lås op for adgang til alle nyhedsbreve ved at blive en betalingsabonnent.", diff --git a/ghost/i18n/locales/de-CH/portal.json b/ghost/i18n/locales/de-CH/portal.json index 03895b4d89de..e871df48fbe2 100644 --- a/ghost/i18n/locales/de-CH/portal.json +++ b/ghost/i18n/locales/de-CH/portal.json @@ -138,6 +138,7 @@ "Sign out": "Abmelden", "Sign up": "Registrieren", "Signup error: Invalid link": "Fehler bei der Registrierung: Ungültiger Link.", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Entschuldigen Sie, das hat leider nicht funktioniert.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Der Zugang zu diesem Inhalt ist eingeschränkt. Bitte kontaktieren Sie uns, wenn Sie Zugang wünschen.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Probieren Sie uns für {{amount}} Tage kostenlos aus. Danach folgt ein Abo zum Preis von {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Schalten Sie mit einem Abo den Zugang zu allen Newslettern frei.", diff --git a/ghost/i18n/locales/de/portal.json b/ghost/i18n/locales/de/portal.json index 7fe5353ff085..57b1828f9eb5 100644 --- a/ghost/i18n/locales/de/portal.json +++ b/ghost/i18n/locales/de/portal.json @@ -138,6 +138,7 @@ "Sign out": "Abmelden", "Sign up": "Registrieren", "Signup error: Invalid link": "Fehler bei der Registrierung: Ungültiger Link", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Etwas ist schief gelaufen, bitte versuche es später noch einmal.", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Entschuldige, das hat nicht funktioniert.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Beim Erneuern deines Abonnements ist ein Fehler aufgetreten. Bitte versuche es erneut.", "There was an error processing your payment. Please try again.": "Bei der Verarbeitung deiner Zahlung gab es einen Fehler. Bitte versuche es noch einmal.", "There was an error sending the email, please try again": "Beim Versand der E-Mail ist ein Fehler aufgetreten. Bitte versuche es erneut.", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Für diese Seite benötigst du eine Einladung. Bitte kontaktiere den Inhaber.", "This site is not accepting payments at the moment.": "Diese Website nimmt zur Zeit keine Zahlungen entgegen.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Zu viele verschiedene Anmeldeversuche. Versuche es in {{number}} Tagen erneut.", "Too many different sign-in attempts, try again in {{number}} hours": "Zu viele verschiedene Anmeldeversuche. Versuche es in {{number}} Stunden erneut.", "Too many different sign-in attempts, try again in {{number}} minutes": "Zu viele verschiedene Anmeldeversuche. Versuche es in {{number}} Minuten erneut.", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Kostenfreier Testzugang für {{amount}} Tage, danach {{originalPrice}}.", "Unable to initiate checkout session": "Zahlungsabwicklung konnte nicht gestartet werden", "Unlock access to all newsletters by becoming a paid subscriber.": "Schalte den Zugang zu allen Newslettern frei, indem du zahlende/r Abonnent*in wirst.", diff --git a/ghost/i18n/locales/el/portal.json b/ghost/i18n/locales/el/portal.json index 98a6143e0b49..882755e96ee6 100644 --- a/ghost/i18n/locales/el/portal.json +++ b/ghost/i18n/locales/el/portal.json @@ -138,6 +138,7 @@ "Sign out": "Αποσύνδεση", "Sign up": "Εγγραφή", "Signup error: Invalid link": "Σφάλμα εγγραφής: Μη έγκυρος σύνδεσμος", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Συγγνώμη, αυτό δεν λειτούργησε.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Αυτός ο ιστότοπος είναι μόνο με πρόσκληση, επικοινωνήστε με τον ιδιοκτήτη για πρόσβαση.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Δοκιμάστε δωρεάν για {{amount}} ημέρες, μετά {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Ξεκλειδώστε την πρόσβαση σε όλα τα ενημερωτικά δελτία, ενεργοποιόντας την premium συνδρομή.", diff --git a/ghost/i18n/locales/en/portal.json b/ghost/i18n/locales/en/portal.json index 508f16c34662..3d23a769b025 100644 --- a/ghost/i18n/locales/en/portal.json +++ b/ghost/i18n/locales/en/portal.json @@ -138,6 +138,7 @@ "Sign out": "", "Sign up": "", "Signup error: Invalid link": "", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", diff --git a/ghost/i18n/locales/eo/portal.json b/ghost/i18n/locales/eo/portal.json index 7f5724bdf9a5..80d6099ceaa6 100644 --- a/ghost/i18n/locales/eo/portal.json +++ b/ghost/i18n/locales/eo/portal.json @@ -138,6 +138,7 @@ "Sign out": "", "Sign up": "Aliĝu", "Signup error: Invalid link": "", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ĉi tiu retejo estas nur por invitiĝuloj, kontaktu la proprietulo por alireblo.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", diff --git a/ghost/i18n/locales/es/portal.json b/ghost/i18n/locales/es/portal.json index 0dd42ceae6f9..02d4053f7828 100644 --- a/ghost/i18n/locales/es/portal.json +++ b/ghost/i18n/locales/es/portal.json @@ -138,6 +138,7 @@ "Sign out": "Cerrar sesión", "Sign up": "Registrarse", "Signup error: Invalid link": "Error de registro: Enlace inválido", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Lo siento, eso no funcionó.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Hubo un error en continuar la suscripción, inténtalo de nuevo por favor.", "There was an error processing your payment. Please try again.": "Hubo un error procesando tu pago. Intentalo de nuevvo por favor.", "There was an error sending the email, please try again": "Hubo un error enviando el correo electrónico, intentalo de nuevo por favor.", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Este sitio es solo por invitación, contacta al propietario para obtener acceso.", "This site is not accepting payments at the moment.": "Este sitio no acepta pagos en este momento.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Demasiados intentos de iniciar sesión, intentalo de nuevo en {{number}} días", "Too many different sign-in attempts, try again in {{number}} hours": "Demasiados intentos de iniciar sesión, intentalo de nuevo en {{number}} horas", "Too many different sign-in attempts, try again in {{number}} minutes": "Demasiados intentos de iniciar sesión, intentalo de nuevo en {{number}} minutos", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prueba gratis por {{amount}} dias, luego {{originalPrice}}.", "Unable to initiate checkout session": "No se pudo iniciar la sesión de pago", "Unlock access to all newsletters by becoming a paid subscriber.": "Desbloquea el acceso a todos los boletines convirtiéndote en un suscriptor pago.", diff --git a/ghost/i18n/locales/et/portal.json b/ghost/i18n/locales/et/portal.json index 5d70c09a7db1..09f1d4a4e0c2 100644 --- a/ghost/i18n/locales/et/portal.json +++ b/ghost/i18n/locales/et/portal.json @@ -138,6 +138,7 @@ "Sign out": "Logi välja", "Sign up": "Registreeru", "Signup error: Invalid link": "Registreerimise viga: Vigane link", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Midagi läks valesti, palun proovige hiljem uuesti.", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "See sait on ainult kutsetega, juurdepääsu saamiseks võtke ühendust omanikuga.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Proovige tasuta {{amount}} päeva, seejärel {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Avage juurdepääs kõigile uudiskirjadele, hakates tasuliseks tellijaks.", diff --git a/ghost/i18n/locales/fa/portal.json b/ghost/i18n/locales/fa/portal.json index c8d227e1b70b..a3a894cc097a 100644 --- a/ghost/i18n/locales/fa/portal.json +++ b/ghost/i18n/locales/fa/portal.json @@ -138,6 +138,7 @@ "Sign out": "بیرون رفتن", "Sign up": "ثبت نام", "Signup error: Invalid link": "خطای ثبت نام: پیوند معتبر نیست", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "پوزش می\u200cخواهیم، آن کار انجام نشد.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "دسترسی به این وب\u200cسایت نیازمند دعوت\u200cنامه است، با مالک آن برای دریافت دسترسی تماس بگیرید.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "برای {{amount}} روز به صورت رایگان امتحان کنید، سپس با قیمت {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "با دریافت اشتراک پولی به شما دسترسی به تمامی خبرنامه\u200cها داده می\u200cشود.", diff --git a/ghost/i18n/locales/fi/portal.json b/ghost/i18n/locales/fi/portal.json index a7714935494d..818edb3e33eb 100644 --- a/ghost/i18n/locales/fi/portal.json +++ b/ghost/i18n/locales/fi/portal.json @@ -138,6 +138,7 @@ "Sign out": "Kirjaudu ulos", "Sign up": "Rekisteröidy", "Signup error: Invalid link": "Virhe rekisteröinnissä: Linkki ei toimi", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Anteeksi, tämä ei onnistunut", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Tämä sivu on vain kutsutuille, ota yhteyttä omistajaan saadaksesi pääsyoikeuden.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Kokeile ilmaiseksi {{amount}} päivää, sen jälkeen hinta on {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Avaa pääsy kaikkiin uutiskirjeisiin maksullisella tilauksella.", diff --git a/ghost/i18n/locales/fr/portal.json b/ghost/i18n/locales/fr/portal.json index 21e6f2b607e5..6898beb10780 100644 --- a/ghost/i18n/locales/fr/portal.json +++ b/ghost/i18n/locales/fr/portal.json @@ -138,6 +138,7 @@ "Sign out": "Se déconnecter", "Sign up": "S’inscrire", "Signup error: Invalid link": "Erreur lors de l'inscription : le lien est invalide", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Quelque chose s'est mal passé, veuillez réessayer plus tard.", "Sorry, no recommendations are available right now.": "Désolé, aucune recommandation n'est disponible pour le moment.", "Sorry, that didn’t work.": "Désolé, cela n'a pas fonctionné.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Une erreur s'est produite lors de la prolongation de votre abonnement, veuillez réessayer.", "There was an error processing your payment. Please try again.": "Une erreur s'est produite lors du traitement de votre paiement. Veuillez réessayer.", "There was an error sending the email, please try again": "Une erreur s'est produite lors de l'envoi de l'e-mail, veuillez réessayer.", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ce site est réservé aux invités. Veuillez écrire au propriétaire pour en demander l'accès.", "This site is not accepting payments at the moment.": "Ce site n'accepte pas les paiements pour le moment.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Trop de tentatives de connexion différentes, réessayez dans {{number}} jours", "Too many different sign-in attempts, try again in {{number}} hours": "Trop de tentatives de connexion différentes, réessayez dans {{number}} heures", "Too many different sign-in attempts, try again in {{number}} minutes": "Trop de tentatives de connexion différentes, réessayez dans {{number}} minutes", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Essayez gratuitement pendant {{amount}} jours, puis {{originalPrice}}.", "Unable to initiate checkout session": "Impossible d'initier une session de paiement", "Unlock access to all newsletters by becoming a paid subscriber.": "Débloquez l'accès à toutes les newsletters en souscrivant un abonnement payant.", diff --git a/ghost/i18n/locales/gd/portal.json b/ghost/i18n/locales/gd/portal.json index 007908c4babd..880ef6d58342 100644 --- a/ghost/i18n/locales/gd/portal.json +++ b/ghost/i18n/locales/gd/portal.json @@ -138,6 +138,7 @@ "Sign out": "Clàraich a-mach", "Sign up": "Clàraich", "Signup error: Invalid link": "Mearachd: Ceangal mì-dhligheach", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Thachair mearachd, feuch a-rithist an ceann greis.", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Duilich, cha do dh’obraich sin.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Thachair mearachd fhad 's a bhathar a' làimhseachadh a' phàighidh agad. Feuch a-rithist an ceann greis.", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Feumar cuireadh airson an làrach-lìn seo, leig fios dhan rianaire ma tha thu ag iarraidh cothrom-inntrigidh.", "This site is not accepting payments at the moment.": "Chan eil an làrach seo a' gabhail ri phàighidhean an-dràsta.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "An-asgaidh airson {{amount}}l, agus {{originalPrice}} an dèidh sin ", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Tig nad bhall phaighte gus cothrom fhaighinn air na cuairt-litrichean gu lèir.", diff --git a/ghost/i18n/locales/he/portal.json b/ghost/i18n/locales/he/portal.json index 19c9163cf736..f56647206323 100644 --- a/ghost/i18n/locales/he/portal.json +++ b/ghost/i18n/locales/he/portal.json @@ -138,6 +138,7 @@ "Sign out": "יציאה", "Sign up": "הרשמה", "Signup error: Invalid link": "שגיאת הרשמה: לינק לא תקין", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "משהו השתבש, נסו שוב מאוחר יותר.", "Sorry, no recommendations are available right now.": "מצטערים אך אין המלצות זמינות כעת.", "Sorry, that didn’t work.": "מצטערים, זה לא עבד", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "שגיאה בהמשך המנוי שלכם, נסו שוב.", "There was an error processing your payment. Please try again.": "שגיאה בעיבוד התשלום שלכם. נסו שוב.", "There was an error sending the email, please try again": "שגיאה בשליחת המייל, נסו שוב", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "אתר זה פתוח למוזמנים בלבד, פנו לבעל האתר לגישה.", "This site is not accepting payments at the moment.": "אתר זה לא מקבל תשלומים כרגע.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "יותר מדי ניסיונות כניסה שונים, נסו שוב בעוד {{number}} ימים", "Too many different sign-in attempts, try again in {{number}} hours": "יותר מדי ניסיונות כניסה שונים, נסו שוב בעוד {{number}} שעות", "Too many different sign-in attempts, try again in {{number}} minutes": "יותר מדי ניסיונות כניסה שונים, נסו שוב בעוד {{number}} דקות", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "נסו בחינם למשך {{amount}} ימים, ואז {{originalPrice}}.", "Unable to initiate checkout session": "בעיה בהתחלת סשן קופה", "Unlock access to all newsletters by becoming a paid subscriber.": "פתחו גישה לכל הניוזלטרים על ידי הפכתכם למנוי בתשלום.", diff --git a/ghost/i18n/locales/hi/portal.json b/ghost/i18n/locales/hi/portal.json index c3cb361240eb..10195c9a7f50 100644 --- a/ghost/i18n/locales/hi/portal.json +++ b/ghost/i18n/locales/hi/portal.json @@ -138,6 +138,7 @@ "Sign out": "साइन आउट करें", "Sign up": "साइन अप करें", "Signup error: Invalid link": "साइनअप त्रुटि: अमान्य लिंक", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "क्षमा करें, वह काम नहीं किया।", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "यह साइट केवल निमंत्रण द्वारा है, पहुँच के लिए मालिक से संपर्क करें।", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}} दिनों के लिए मुफ्त प्रयास करें, फिर {{originalPrice}}।", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "एक सशुल्क सदस्य बनकर सभी न्यूज़लेटर्स तक पहुंच अनलॉक करें।", diff --git a/ghost/i18n/locales/hr/portal.json b/ghost/i18n/locales/hr/portal.json index 937fb337dd95..d7a413e2d71a 100644 --- a/ghost/i18n/locales/hr/portal.json +++ b/ghost/i18n/locales/hr/portal.json @@ -138,6 +138,7 @@ "Sign out": "Odjava", "Sign up": "Registracija", "Signup error: Invalid link": "Greška prilikom prijave. Neispravan link", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Žao nam je, to nije uspjelo.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ove stranice su samo za članove, kontaktirajte vlasnika kako biste dobili pristup.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Probajte besplatno na {{amount}} dana, zatim {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Otključajte pristup svim newsletterima plaćanjem pretplate.", diff --git a/ghost/i18n/locales/hu/portal.json b/ghost/i18n/locales/hu/portal.json index f304651bcbbe..0eda99b161b0 100644 --- a/ghost/i18n/locales/hu/portal.json +++ b/ghost/i18n/locales/hu/portal.json @@ -138,6 +138,7 @@ "Sign out": "Kijelentkezés", "Sign up": "Regisztráció", "Signup error: Invalid link": "Regisztrációs hiba: érvénytelen link", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Sajnáljuk, ez nem működött.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "A website csak meghívóval látogatható. Meghívóért lépjen kapcsolatba az oldal tulajdonosával!", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Próbálja ki ingyen {{amount}} napig, utána {{originalPrice}}", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Előfizetéssel hozzáférhet minden hírlevélhez!", diff --git a/ghost/i18n/locales/id/portal.json b/ghost/i18n/locales/id/portal.json index 5de578cae554..eb91e95a6382 100644 --- a/ghost/i18n/locales/id/portal.json +++ b/ghost/i18n/locales/id/portal.json @@ -138,6 +138,7 @@ "Sign out": "Keluar", "Sign up": "Daftar", "Signup error: Invalid link": "Kesalahan pendaftaran: Tautan tidak valid", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Terjadi kesalahan, silakan coba lagi nanti.", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Maaf, itu tidak berhasil.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Terjadi kesalahan saat melanjutkan langganan Anda, harap coba lagi.", "There was an error processing your payment. Please try again.": "Terjadi kesalahan saat memproses pembayaran Anda. Harap coba lagi.", "There was an error sending the email, please try again": "Terjadi kesalahan saat mengirim email, harap coba lagi", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Situs ini hanya untuk yang diundang, hubungi pemiliknya untuk mendapatkan akses.", "This site is not accepting payments at the moment.": "Situs ini tidak menerima pembayaran saat ini.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Terlalu banyak percobaan masuk, coba lagi dalam {{number}} hari", "Too many different sign-in attempts, try again in {{number}} hours": "Terlalu banyak percobaan masuk, coba lagi dalam {{number}} jam", "Too many different sign-in attempts, try again in {{number}} minutes": "Terlalu banyak percobaan masuk, coba lagi dalam {{number}} menit", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Coba gratis selama {{amount}} hari, kemudian {{originalPrice}}.", "Unable to initiate checkout session": "Tidak dapat memulai sesi checkout", "Unlock access to all newsletters by becoming a paid subscriber.": "Buka akses ke semua buletin dengan menjadi pelanggan berbayar.", diff --git a/ghost/i18n/locales/is/portal.json b/ghost/i18n/locales/is/portal.json index 9b5b2a17b8bf..825efe7547ec 100644 --- a/ghost/i18n/locales/is/portal.json +++ b/ghost/i18n/locales/is/portal.json @@ -138,6 +138,7 @@ "Sign out": "Útskráning", "Sign up": "Nýskráning", "Signup error: Invalid link": "Villa við nýskráningu: Ógildur hlekkur", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Þetta virkaði því miður ekki.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Aðgangur krefst boðsmiða, hafið samband við eiganda síðunnar til að fá aðgang.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prófaðu í {{amount}} daga án endurgjalds og síðan fyrir {{originalPrice}}", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Fáðu aðgang að öllum fréttabréfum með því að gerast áskrifandi.", diff --git a/ghost/i18n/locales/it/portal.json b/ghost/i18n/locales/it/portal.json index 2e5738f4051a..730c303fd242 100644 --- a/ghost/i18n/locales/it/portal.json +++ b/ghost/i18n/locales/it/portal.json @@ -138,6 +138,7 @@ "Sign out": "Esci", "Sign up": "Iscriviti", "Signup error: Invalid link": "Errore di accesso: link invalido", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Qualcosa è andato storto, riprova più tardi.", "Sorry, no recommendations are available right now.": "Ci dispiace, al momento non ci sono consigli disponibili.", "Sorry, that didn’t work.": "Ci dispiace, non ha funzionato.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "C'è stato un errore nella continuazione del tuo abbonamento, riprova per favore.", "There was an error processing your payment. Please try again.": "C'è stato un errore durante l’elaborazione del tuo pagamento. Riprova per favore.", "There was an error sending the email, please try again": "C'è stato un errore nell'invio dell'e-mail, per favore riprova", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Questo sito è accessibile solo su invito, contatta il proprietario per poter accedere.", "This site is not accepting payments at the moment.": "Questo sito non accetta pagamenti al momento.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Troppi tentativi di accesso, riprova fra {{number}} giorni", "Too many different sign-in attempts, try again in {{number}} hours": "Troppi tentativi di accesso, riprova fra {{number}} ore", "Too many different sign-in attempts, try again in {{number}} minutes": "Troppi tentativi di accesso, riprova fra {{number}} minuti", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prova gratis per {{amount}} giorni, poi {{originalPrice}}.", "Unable to initiate checkout session": "Impossibile iniziare il checkout", "Unlock access to all newsletters by becoming a paid subscriber.": "Abbonati per sbloccare l'accesso a tutte le newsletter.", diff --git a/ghost/i18n/locales/ja/portal.json b/ghost/i18n/locales/ja/portal.json index 9b3182cca723..e28a0fc58a2d 100644 --- a/ghost/i18n/locales/ja/portal.json +++ b/ghost/i18n/locales/ja/portal.json @@ -138,6 +138,7 @@ "Sign out": "ログアウト", "Sign up": "新規登録", "Signup error: Invalid link": "エラー: 無効なリンク", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "申し訳ありませんが、うまくいきませんでした。", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "このサイトは招待制です。アクセスするにはオーナーに連絡してください。", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}}日間無料でお試しください、その後は{{originalPrice}}です。", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "有料の購読者になることで、すべてのニュースレターへのアクセスが可能になります。", diff --git a/ghost/i18n/locales/ko/portal.json b/ghost/i18n/locales/ko/portal.json index 2fd55603b09c..ba7b21427e1d 100644 --- a/ghost/i18n/locales/ko/portal.json +++ b/ghost/i18n/locales/ko/portal.json @@ -138,6 +138,7 @@ "Sign out": "로그아웃", "Sign up": "가입", "Signup error: Invalid link": "가입 오류: 잘못된 링크", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "문제가 발생했어요. 나중에 다시 시도해 주세요.", "Sorry, no recommendations are available right now.": "죄송해요. 현재 추천할 만한 콘텐츠가 없어요.", "Sorry, that didn’t work.": "죄송해요. 작동하지 않았어요.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "구독 계속하기 중 오류가 발생했어요. 다시 시도해 주세요.", "There was an error processing your payment. Please try again.": "결제 처리 중 오류가 발생했어요. 다시 시도해 주세요.", "There was an error sending the email, please try again": "이메일 전송 중 오류가 발생했어요. 다시 시도해 주세요", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "위 사이트는 초대된 사용자만 사용이 가능해요. 접근을 위해서는 관리자에게 연락해 주세요.", "This site is not accepting payments at the moment.": "현재 이 사이트는 결제를 받지 않고 있어요.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "너무 많은 로그인 시도를 하셨어요. {{number}}일 후에 다시 시도해 주세요", "Too many different sign-in attempts, try again in {{number}} hours": "너무 많은 로그인 시도를 하셨어요. {{number}}시간 후에 다시 시도해 주세요", "Too many different sign-in attempts, try again in {{number}} minutes": "너무 많은 로그인 시도를 하셨어요. {{number}}분 후에 다시 시도해 주세요", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}}일 동안 무료로 사용한 후 {{originalPrice}}로 결제해 주세요.", "Unable to initiate checkout session": "결제 세션을 시작할 수 없어요", "Unlock access to all newsletters by becoming a paid subscriber.": "유료 구독자가 되어 모든 뉴스레터에 접근해 주세요.", diff --git a/ghost/i18n/locales/kz/portal.json b/ghost/i18n/locales/kz/portal.json index ce82ba45e3db..21571bf57169 100644 --- a/ghost/i18n/locales/kz/portal.json +++ b/ghost/i18n/locales/kz/portal.json @@ -138,6 +138,7 @@ "Sign out": "Шығу", "Sign up": "Тіркелу", "Signup error: Invalid link": "Тіркелу қатесі: Жарамсыз сілтеме", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Өкінішті, бұдан ештеңе шықпады.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Бұл сайтқа тек шақырту бойынша кіруге болады, рұқсат алу үшін иесіне хабарласыңыз.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}} күн тегін қолданып көріңіз, содан кейінгі бағасы {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Ақылы түрде жазылу арқылы барлық ақпараттық бюллетеньдерге қол жеткізіңіз.", diff --git a/ghost/i18n/locales/lt/portal.json b/ghost/i18n/locales/lt/portal.json index e05538188dd1..c3ab17cf9d7f 100644 --- a/ghost/i18n/locales/lt/portal.json +++ b/ghost/i18n/locales/lt/portal.json @@ -138,6 +138,7 @@ "Sign out": "Atsijungti", "Sign up": "Registruotis", "Signup error: Invalid link": "Registracijos klaida: negaliojanti nuoroda", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Atsiprašome, nepavyko.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ši svetainė pasiekiama tik su pakvietimu, susisiekite su savininku dėl prieigos. ", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Išbandykite {{amount}} d. nemokamai, vėliau {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Gaukite prieigą prie visų naujienlaiškių įsigiję mokamą prenumeratą.", diff --git a/ghost/i18n/locales/lv/portal.json b/ghost/i18n/locales/lv/portal.json index 190adb36072e..b6dfdc82287f 100644 --- a/ghost/i18n/locales/lv/portal.json +++ b/ghost/i18n/locales/lv/portal.json @@ -138,6 +138,7 @@ "Sign out": "Izrakstīties", "Sign up": "Pierakstīties", "Signup error: Invalid link": "Reģistrācijas kļūda: nederīga saite", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Radās problēma. Lūdzu, vēlāk mēģiniet vēlreiz.", "Sorry, no recommendations are available right now.": "Diemžēl pašlaik ieteikumi nav pieejami.", "Sorry, that didn’t work.": "Atvainojiet, tas nedarbojās.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Turpinot abonementu, radās kļūda. Lūdzu, mēģiniet vēlreiz.", "There was an error processing your payment. Please try again.": "Apstrādājot jūsu maksājumu, radās kļūda. Lūdzu, mēģiniet vēlreiz.", "There was an error sending the email, please try again": "Nosūtot e-pasta ziņojumu, radās kļūda. Lūdzu, mēģiniet vēlreiz", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Šī vietne ir paredzēta tikai ielūgumam. Lai iegūtu piekļuvi, sazinieties ar īpašnieku.", "This site is not accepting payments at the moment.": "Šī vietne pašlaik nepieņem maksājumus.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Pārāk daudz dažādu pierakstīšanās mēģinājumu, mēģiniet vēlreiz pēc {{number}}\u00a0dienām", "Too many different sign-in attempts, try again in {{number}} hours": "Pārāk daudz dažādu pierakstīšanās mēģinājumu. Mēģiniet vēlreiz pēc {{number}}\u00a0stundām", "Too many different sign-in attempts, try again in {{number}} minutes": "Pārāk daudz dažādu pierakstīšanās mēģinājumu, mēģiniet vēlreiz pēc {{number}}\u00a0minūtēm", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Mēģiniet bez maksas {{amount}}\u00a0dienas, pēc tam {{original Price}}.", "Unable to initiate checkout session": "Nevar uzsākt norēķināšanās sesiju", "Unlock access to all newsletters by becoming a paid subscriber.": "Atbloķējiet piekļuvi visiem biļeteniem, kļūstot par maksas abonentu.", diff --git a/ghost/i18n/locales/mk/portal.json b/ghost/i18n/locales/mk/portal.json index 3451e7300d6e..4b38c32ee053 100644 --- a/ghost/i18n/locales/mk/portal.json +++ b/ghost/i18n/locales/mk/portal.json @@ -138,6 +138,7 @@ "Sign out": "Одјавете се", "Sign up": "Регистрирајте се", "Signup error: Invalid link": "Грешка при регистрација: Невалиден линк", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Се извинуваме, тоа не проработи.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Оваа страница е достапна само со покана. За пристап контактирајте го сопственикот.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Пробајте бесплатно за {{amount}} денови, потоа по цена од {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Добијте пристап до сите билтени преку платена претплата.", diff --git a/ghost/i18n/locales/mn/portal.json b/ghost/i18n/locales/mn/portal.json index 97d23717539d..6737f41fde53 100644 --- a/ghost/i18n/locales/mn/portal.json +++ b/ghost/i18n/locales/mn/portal.json @@ -138,6 +138,7 @@ "Sign out": "", "Sign up": "Бүртгүүлэх", "Signup error: Invalid link": "", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Энэхүү сайт руу зөвхөн урилгаар нэвтрэх боломжтой тул та админд нь хандана уу.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", diff --git a/ghost/i18n/locales/ms/portal.json b/ghost/i18n/locales/ms/portal.json index bd2111dea79d..313d902d2d7b 100644 --- a/ghost/i18n/locales/ms/portal.json +++ b/ghost/i18n/locales/ms/portal.json @@ -138,6 +138,7 @@ "Sign out": "Log keluar", "Sign up": "Daftar", "Signup error: Invalid link": "Ralat daftar: Pautan tidak sah", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Maaf, itu tidak berfungsi.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Laman web ini hanya untuk jemputan, hubungi pemilik untuk akses.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Cuba secara percuma selama {{amount}} hari, kemudian {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Buka akses ke semua newsletter dengan menjadi pelanggan berbayar.", diff --git a/ghost/i18n/locales/ne/portal.json b/ghost/i18n/locales/ne/portal.json index 508f16c34662..3d23a769b025 100644 --- a/ghost/i18n/locales/ne/portal.json +++ b/ghost/i18n/locales/ne/portal.json @@ -138,6 +138,7 @@ "Sign out": "", "Sign up": "", "Signup error: Invalid link": "", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", diff --git a/ghost/i18n/locales/nl/portal.json b/ghost/i18n/locales/nl/portal.json index e16b198b6218..37a12fc49eb5 100644 --- a/ghost/i18n/locales/nl/portal.json +++ b/ghost/i18n/locales/nl/portal.json @@ -138,6 +138,7 @@ "Sign out": "Uitloggen", "Sign up": "Registreren", "Signup error: Invalid link": "Registratiefout: Ongeldige link", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Er ging iets mis, probeer het later opnieuw.", "Sorry, no recommendations are available right now.": "Sorry, er zijn momenteel geen aanbevelingen beschikbaar.", "Sorry, that didn’t work.": "Sorry, dat werkte niet.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Er was een fout bij het voortzetten van je abonnement, probeer het opnieuw.", "There was an error processing your payment. Please try again.": "Er was een fout bij het verwerken van je betaling, probeer het opnieuw.", "There was an error sending the email, please try again": "Er was een fout bij het verzenden van de e-mail, probeer het opnieuw", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Deze site is alleen toegankelijk op uitnodiging, neem contact op met de eigenaar.", "This site is not accepting payments at the moment.": "Deze site accepteert momenteel geen betalingen.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Te veel verschillende inlogpogingen, probeer het opnieuw over {{number}} dagen.", "Too many different sign-in attempts, try again in {{number}} hours": "Te veel verschillende inlogpogingen, probeer het opnieuw over {{number}} uur.", "Too many different sign-in attempts, try again in {{number}} minutes": "Te veel verschillende inlogpogingen, probeer het opnieuw over {{number}} minuten.", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Probeer gratis voor {{amount}} dagen, daarna {{originalPrice}}.", "Unable to initiate checkout session": "Kan afrekeningssessie niet starten", "Unlock access to all newsletters by becoming a paid subscriber.": "Ontgrendel toegang tot alle nieuwsbrieven door een betalende abonnee te worden.", diff --git a/ghost/i18n/locales/nn/portal.json b/ghost/i18n/locales/nn/portal.json index d3c06a17261d..62eff84c4774 100644 --- a/ghost/i18n/locales/nn/portal.json +++ b/ghost/i18n/locales/nn/portal.json @@ -138,6 +138,7 @@ "Sign out": "Logg ut", "Sign up": "Registrer deg", "Signup error: Invalid link": "Registreringsfeil: Ugyldig lenke", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Beklagar, det verka ikkje.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Denne sida er kun for inviterte, ta kontakt med eigaren for tilgang.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prøv gratis i {{amount}} dagar, deretter {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Få tilgang til alle nyheitsbreva med å bli ein betalande abonnent.", diff --git a/ghost/i18n/locales/no/portal.json b/ghost/i18n/locales/no/portal.json index 55f85c8d6460..07dc34a9251f 100644 --- a/ghost/i18n/locales/no/portal.json +++ b/ghost/i18n/locales/no/portal.json @@ -138,6 +138,7 @@ "Sign out": "Logg ut", "Sign up": "Opprett bruker", "Signup error: Invalid link": "Feil ved registrering: Ugyldig lenke", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Noe gikk galt. Prøv igjen senere.", "Sorry, no recommendations are available right now.": "Beklager, ingen anbefalinger er tilgjengelige for øyeblikket.", "Sorry, that didn’t work.": "Beklager, det fungerte ikke", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "En feil oppstod ved fornyelse av abonnementet, vennligst prøv igjen.", "There was an error processing your payment. Please try again.": "Det oppsto en feil under behandling av betalingen din. Vennligst prøv igjen.", "There was an error sending the email, please try again": "En feil oppstod ved sending av e-posten, vennligst prøv igjen", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Denne nettsiden er kun for inviterte. Kontakt eieren for invitasjon.", "This site is not accepting payments at the moment.": "Denne nettsiden godtar ikke betalinger for øyeblikket.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "For mange påloggingsforsøk, prøv igjen om {{number}} dager.", "Too many different sign-in attempts, try again in {{number}} hours": "For mange påloggingsforsøk, prøv igjen om {{number}} timer.", "Too many different sign-in attempts, try again in {{number}} minutes": "For mange påloggingsforsøk, prøv igjen om {{number}} minutter.", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prøv gratis i {{amount}} dager, deretter {{originalPrice}}.", "Unable to initiate checkout session": "Kunne ikke starte utsjekkingssesjon", "Unlock access to all newsletters by becoming a paid subscriber.": "Få tilgang til alle nyhetsbrev ved å bli betalende abonnent.", diff --git a/ghost/i18n/locales/pl/portal.json b/ghost/i18n/locales/pl/portal.json index 518af111ec0f..ac4dbb3d05e1 100644 --- a/ghost/i18n/locales/pl/portal.json +++ b/ghost/i18n/locales/pl/portal.json @@ -138,6 +138,7 @@ "Sign out": "Wyloguj się", "Sign up": "Zarejestruj się", "Signup error: Invalid link": "Błąd rejestracji: Nieprawidłowy link", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Przepraszamy, to nie zadziałało.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ta strona posiada zamknięty dostęp. Skontaktuj się z właścicielem, aby uzyskać dostęp.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Wypróbuj za darmo przez {{amount}} dni, później {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Zostań płatnym subskrybentem i odblokuj dostęp do wszystkich biuletynów.", diff --git a/ghost/i18n/locales/pt-BR/portal.json b/ghost/i18n/locales/pt-BR/portal.json index 94cbdeb8c25e..21efbb24567e 100644 --- a/ghost/i18n/locales/pt-BR/portal.json +++ b/ghost/i18n/locales/pt-BR/portal.json @@ -138,6 +138,7 @@ "Sign out": "Sair", "Sign up": "Cadastrar", "Signup error: Invalid link": "Erro de inscrição: link inválido", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Algo deu errado, tente novamente mais tarde.", "Sorry, no recommendations are available right now.": "Desculpe, não há recomendações disponíveis no momento.", "Sorry, that didn’t work.": "Desculpe, isso não funcionou.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Houve um erro ao continuar sua assinatura, por favor, tente novamente.", "There was an error processing your payment. Please try again.": "Houve um erro ao processar seu pagamento. Por favor, tente novamente.", "There was an error sending the email, please try again": "Houve um erro ao enviar o e-mail, por favor, tente novamente.", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Este site é apenas para convidados. Contate o proprietário para obter acesso.", "This site is not accepting payments at the moment.": "Este site não está aceitando pagamentos no momento.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Muitas tentativas de login diferentes, tente novamente em {{number}} dias.", "Too many different sign-in attempts, try again in {{number}} hours": "Muitas tentativas de login diferentes, tente novamente em {{number}} horas.", "Too many different sign-in attempts, try again in {{number}} minutes": "Muitas tentativas de login diferentes, tente novamente em {{number}} minutos.", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Experimente grátis por {{amount}} dias, depois {{originalPrice}}.", "Unable to initiate checkout session": "Não foi possível iniciar a sessão de pagamento.", "Unlock access to all newsletters by becoming a paid subscriber.": "Desbloqueie o acesso a todas as newsletters se tornando um assinante pago.", diff --git a/ghost/i18n/locales/pt/portal.json b/ghost/i18n/locales/pt/portal.json index 8d7766791702..8dff1af3cc41 100644 --- a/ghost/i18n/locales/pt/portal.json +++ b/ghost/i18n/locales/pt/portal.json @@ -138,6 +138,7 @@ "Sign out": "Sair", "Sign up": "Registar", "Signup error: Invalid link": "Erro de inscrição: ligação inválida", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Temos um erro em mãos, tente por favor mais tarde.", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Desculpe, mas isso não funcionou.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Houve um problema ao processar o seu pagamento. Tente novamente por favor.", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "O acesso a este site é feito apenas por convite. Entre em contacto com o proprietário para obter acesso.", "This site is not accepting payments at the moment.": "Este site não está a aceitar pagamentos de momento", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Experimente grátis por {{amount}} dias, depois {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Desbloqueie o acesso a todas as newsletters tornando-se um assinante pago.", diff --git a/ghost/i18n/locales/ro/portal.json b/ghost/i18n/locales/ro/portal.json index dce5d968ca71..979cb438c13a 100644 --- a/ghost/i18n/locales/ro/portal.json +++ b/ghost/i18n/locales/ro/portal.json @@ -138,6 +138,7 @@ "Sign out": "Deconectare", "Sign up": "Înregistrare", "Signup error: Invalid link": "Eroare la înregistrare: Link invalid", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Ne pare rău, nu a funcționat.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Acest site este disponibil doar pe bază de invitație, contactează proprietarul pentru acces.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Prea multe încercări de autentificare diferite, încearcă din nou în {{number}} zile.", "Too many different sign-in attempts, try again in {{number}} hours": "Prea multe încercări de autentificare diferite, încearcă din nou în {{number}} ore.", "Too many different sign-in attempts, try again in {{number}} minutes": "Prea multe încercări de autentificare diferite, încearcă din nou în {{number}} minute.", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Încearcă gratuit pentru {{amount}} zile, apoi {{originalPrice}}.", "Unable to initiate checkout session": "Nu pot iniția sesiunea de plată.", "Unlock access to all newsletters by becoming a paid subscriber.": "Deblochează accesul la toate buletinele informative devenind un abonat plătit.", diff --git a/ghost/i18n/locales/ru/portal.json b/ghost/i18n/locales/ru/portal.json index c9525ab915e5..4f9150710ea7 100644 --- a/ghost/i18n/locales/ru/portal.json +++ b/ghost/i18n/locales/ru/portal.json @@ -138,6 +138,7 @@ "Sign out": "Выйти", "Sign up": "Зарегистрироваться", "Signup error: Invalid link": "Ошибка регистрации: Неверная или просроченная ссылка", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Что-то пошло не так, попробуйте ещё раз позже.", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Извините, это не сработало.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Произошла ошибка при обработке вашего платежа. Попробуйте ещё раз.", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Доступ к материалам этого сайта возможен только по приглашению. Для получения доступа свяжитесь с владельцем сайта.", "This site is not accepting payments at the moment.": "В данный момент сайт не принимает платежи.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Попробуйте бесплатно в течение {{amount}} дня(ей), затем за {{original Price}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Получите доступ ко всем рассылкам, оформив платную подписку.", diff --git a/ghost/i18n/locales/si/portal.json b/ghost/i18n/locales/si/portal.json index 4ccedfa27b9a..d815680b02ae 100644 --- a/ghost/i18n/locales/si/portal.json +++ b/ghost/i18n/locales/si/portal.json @@ -138,6 +138,7 @@ "Sign out": "Sign out වෙන්න", "Sign up": "ලියාපදිංචි වෙන්න", "Signup error: Invalid link": "Signup වීම අසාර්ථකයි: වැරදි link එකකි", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "සමාවෙන්න, නමුත් එය සාර්ථක වූයේ නැත.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "මෙම වෙබ් අඩවිය ආරාධිතයන් සඳහා පමණි, ප්\u200dරවේශ වීම සඳහා හිමිකරු අමතන්න.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "දින {{amount}}ක් නොමිලයේ භාවිතා කරන්න, ඉන් පසුව {{originalPrice}}ක් පමණි.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Paid subscriber කෙනෙකු වීම හරහා සියළුම newsletters වලට access ලබාගන්න.", diff --git a/ghost/i18n/locales/sk/portal.json b/ghost/i18n/locales/sk/portal.json index 36486b2eb3bb..da435dfc8340 100644 --- a/ghost/i18n/locales/sk/portal.json +++ b/ghost/i18n/locales/sk/portal.json @@ -138,6 +138,7 @@ "Sign out": "Odhlásiť", "Sign up": "Registrovať", "Signup error: Invalid link": "Chyba registrácie: Neplatný odkaz", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Prepáčte, toto nezafungovalo.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Táto stránka je iba pre pozvaných úžívateľov, kontaktujte vlastníka stránky.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Vyskúšajte zadarmo na {{amount}} dní, potom {{originalPrice}}", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", diff --git a/ghost/i18n/locales/sl/portal.json b/ghost/i18n/locales/sl/portal.json index 9121d3779904..43021afa7934 100644 --- a/ghost/i18n/locales/sl/portal.json +++ b/ghost/i18n/locales/sl/portal.json @@ -138,6 +138,7 @@ "Sign out": "", "Sign up": "Registracija", "Signup error: Invalid link": "", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "To spletno mesto je dostopno samo s povabilom, obrnite se na lastnika.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", diff --git a/ghost/i18n/locales/sq/portal.json b/ghost/i18n/locales/sq/portal.json index e42367d14468..91491e65032b 100644 --- a/ghost/i18n/locales/sq/portal.json +++ b/ghost/i18n/locales/sq/portal.json @@ -138,6 +138,7 @@ "Sign out": "Dil", "Sign up": "Rregjistrohu", "Signup error: Invalid link": "Gabim ne rregjistrim: Link i pavlefshem", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Na vjen keq, kjo nuk funksionoi.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Kjo faqe eshte vetem me ftesa, kontaktoni zoteruesin per akses.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Provo falas per {{amount}} dite, pastaj {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Zhbllokoni aksesin per te gjitha buletinet duke u bere nje abonues me pagese.", diff --git a/ghost/i18n/locales/sr-Cyrl/portal.json b/ghost/i18n/locales/sr-Cyrl/portal.json index 858400556f2f..6377cca66c05 100644 --- a/ghost/i18n/locales/sr-Cyrl/portal.json +++ b/ghost/i18n/locales/sr-Cyrl/portal.json @@ -138,6 +138,7 @@ "Sign out": "Одјавите се", "Sign up": "Пријавите се", "Signup error: Invalid link": "Грешка при пријави: Неважећи линк", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Нешто је пошло наопако, молимо вас покушајте касније.", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Извините, то није успело.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Дошло је до грешке при обради ваше уплате. Молимо вас покушајте поново.", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Овај сајт је само на позив, контактирајте власника ради приступа.", "This site is not accepting payments at the moment.": "Овај сајт тренутно не прихвата уплате.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Пробајте бесплатно за {{amount}} дана, након тога {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Откључајте приступ свим билтенима тако што ћете постати плаћени претплатник.", diff --git a/ghost/i18n/locales/sr/portal.json b/ghost/i18n/locales/sr/portal.json index 586908dcbfa1..7f8db1768626 100644 --- a/ghost/i18n/locales/sr/portal.json +++ b/ghost/i18n/locales/sr/portal.json @@ -138,6 +138,7 @@ "Sign out": "", "Sign up": "Registracija", "Signup error: Invalid link": "", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Ovaj sajt je samo za članove, kontaktirajte vlasnika kako bi dobili pristup.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", diff --git a/ghost/i18n/locales/sv/portal.json b/ghost/i18n/locales/sv/portal.json index 55aeecc8e41a..13e91e9e30e4 100644 --- a/ghost/i18n/locales/sv/portal.json +++ b/ghost/i18n/locales/sv/portal.json @@ -138,6 +138,7 @@ "Sign out": "Logga ut", "Sign up": "Få uppdateringar", "Signup error: Invalid link": "Registreringsfel. Länken fungerade inte.", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Något gick fel, vänligen försök igen senare.", "Sorry, no recommendations are available right now.": "Ursäkta, inga rekommendationer finns tillgängliga just nu.", "Sorry, that didn’t work.": "Ursäkta, det fungerande inte.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Det blev fel när din prenumeration skulle fortsättas, vänligen försök igen", "There was an error processing your payment. Please try again.": "Det blev fel när din betalning skulle behandlas, vänligen försök igen", "There was an error sending the email, please try again": "Det blev ett fel när e-posten skulle skickas, vänligen försök igen", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Den här sidan är endast för inbjudna, kontakta ägaren för åtkomst.", "This site is not accepting payments at the moment.": "Den här webbsidan accepterar inte betalningar för tillfället", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "För många olika inloggningsförsök, testa igen om {{number}} dagar.", "Too many different sign-in attempts, try again in {{number}} hours": "För många olika inloggningsförsök, testa igen om {{number}} timmar.", "Too many different sign-in attempts, try again in {{number}} minutes": "För många olika inloggningsförsök, testa igen om {{number}} minuter.", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prova gratis i {{amount}} dagar, sen betalar du {{originalPrice}}.", "Unable to initiate checkout session": "Kan inte initiera utcheckningssession", "Unlock access to all newsletters by becoming a paid subscriber.": "Få tillgång till alla nyhetsbrev genom att bli betalande prenumerant", diff --git a/ghost/i18n/locales/sw/portal.json b/ghost/i18n/locales/sw/portal.json index 218f70a3df21..50257f894120 100644 --- a/ghost/i18n/locales/sw/portal.json +++ b/ghost/i18n/locales/sw/portal.json @@ -138,6 +138,7 @@ "Sign out": "Toka", "Sign up": "Jisajili", "Signup error: Invalid link": "Kosa la usajili: Kiungo batili", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Samahani, hiyo haikufanya kazi.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Tovuti hii ni ya mialiko pekee, wasiliana na mmiliki kupata ufikiaji.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Jaribu bila malipo kwa siku {{amount}}, kisha {{originalPrice}}.", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Fungua ufikiaji wa majarida yote kwa kuwa mwanachama anayelipa.", diff --git a/ghost/i18n/locales/ta/portal.json b/ghost/i18n/locales/ta/portal.json index b7289ef5487a..434c4c3649b9 100644 --- a/ghost/i18n/locales/ta/portal.json +++ b/ghost/i18n/locales/ta/portal.json @@ -138,6 +138,7 @@ "Sign out": "வெளியேறு", "Sign up": "பதிவு செய்", "Signup error: Invalid link": "பதிவு பிழை: தவறான இணைப்பு", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "ஏதோ தவறு நடந்துவிட்டது, பிறகு மீண்டும் முயற்சிக்கவும்.", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "மன்னிக்கவும், அது வேலை செய்யவில்லை.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "உங்கள் சந்தாவைத் தொடர்வதில் பிழை ஏற்பட்டது, மீண்டும் முயற்சிக்கவும்.", "There was an error processing your payment. Please try again.": "உங்கள் கட்டணத்தை செயலாக்குவதில் பிழை ஏற்பட்டது. மீண்டும் முயற்சிக்கவும்.", "There was an error sending the email, please try again": "மின்னஞ்சலை அனுப்புவதில் பிழை ஏற்பட்டது, மீண்டும் முயற்சிக்கவும்", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "இந்த தளம் அழைப்பு மட்டுமே, அணுகலுக்கு உரிமையாளரைத் தொடர்பு கொள்ளவும்.", "This site is not accepting payments at the moment.": "இந்த தளம் தற்போது கட்டணங்களை ஏற்கவில்லை.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "மிக அதிகமான வேறுபட்ட உள்நுழைவு முயற்சிகள், {{number}} நாட்களில் மீண்டும் முயற்சிக்கவும்", "Too many different sign-in attempts, try again in {{number}} hours": "மிக அதிகமான வேறுபட்ட உள்நுழைவு முயற்சிகள், {{number}} மணிநேரங்களில் மீண்டும் முயற்சிக்கவும்", "Too many different sign-in attempts, try again in {{number}} minutes": "மிக அதிகமான வேறுபட்ட உள்நுழைவு முயற்சிகள், {{number}} நிமிடங்களில் மீண்டும் முயற்சிக்கவும்", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}} நாட்களுக்கு இலவசமாக முயற்சிக்கவும், பின்னர் {{originalPrice}}.", "Unable to initiate checkout session": "செக்அவுட் அமர்வைத் தொடங்க முடியவில்லை", "Unlock access to all newsletters by becoming a paid subscriber.": "பணம் செலுத்தும் சந்தாதாரராக மாறி அனைத்து செய்திமடல்களுக்கும் அணுகலைத் திறக்கவும்.", diff --git a/ghost/i18n/locales/th/portal.json b/ghost/i18n/locales/th/portal.json index 92f4be14b6f6..c1290922a325 100644 --- a/ghost/i18n/locales/th/portal.json +++ b/ghost/i18n/locales/th/portal.json @@ -138,6 +138,7 @@ "Sign out": "ออกจากระบบ", "Sign up": "สมัครใช้งาน", "Signup error: Invalid link": "มีข้อผิดพลาดในการสมัครใช้งาน: ลิงก์ไม่ถูกต้อง", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "ขออภัย, ไม่สามารถส่งได้", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "เว็บไซต์นี้สำหรับผู้ได้รับเชิญเท่านั้น โปรดติดต่อเจ้าของเพื่อเข้าถึง", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "ทดลองใช้ฟรี {{amount}} วัน จากนั้นจ่ายเป็น {{ราคาเดิม}}", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "ปลดล็อกการเข้าถึงจดหมายข่าวทั้งหมดโดยสมัครเป็นสมาชิกแบบชำระเงิน", diff --git a/ghost/i18n/locales/tr/portal.json b/ghost/i18n/locales/tr/portal.json index 9d3059588bd7..2e8f82975fd1 100644 --- a/ghost/i18n/locales/tr/portal.json +++ b/ghost/i18n/locales/tr/portal.json @@ -138,6 +138,7 @@ "Sign out": "Çıkış yap", "Sign up": "Kayıt ol", "Signup error: Invalid link": "Kayıt hatası: Geçersiz bağlantı", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Bir şeyler ters gitti, lütfen daha sonra tekrar deneyin.", "Sorry, no recommendations are available right now.": "Üzgünüz, şu anda öneri bulunmamaktadır", "Sorry, that didn’t work.": "Üzgünüm, bu işe yaramadı.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Aboneliğinizi devam ettirirken bir hata oluştu, lütfen tekrar deneyin.", "There was an error processing your payment. Please try again.": "Ödemeniz işlenirken bir hata oluştu. Lütfen tekrar deneyiniz.", "There was an error sending the email, please try again": "E-posta gönderilirken bir hata oluştu, lütfen tekrar deneyin.", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Bu site sadece davetiyesi olanlar içindir, erişim için site sahibiyle iletişime geç.", "This site is not accepting payments at the moment.": "Bu site şu anda ödeme kabul etmemektedir.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Çok fazla farklı giriş denemesi yapıldı, lütfen {{number}} gün sonra tekrar deneyin", "Too many different sign-in attempts, try again in {{number}} hours": "Çok fazla farklı giriş denemesi yapıldı, lütfen {{number}} saat sonra tekrar deneyin", "Too many different sign-in attempts, try again in {{number}} minutes": "Çok fazla farklı giriş denemesi yapıldı, lütfen {{number}} dakika sonra tekrar deneyin", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}} gün ücretsiz deneyin, ardından {{originalPrice}}.", "Unable to initiate checkout session": "Ödeme oturumu başlatılamadı", "Unlock access to all newsletters by becoming a paid subscriber.": "Tüm bültenlere erişimi açmak için ücretli bir abone olun.", diff --git a/ghost/i18n/locales/uk/portal.json b/ghost/i18n/locales/uk/portal.json index 4dc2eb04d087..dbccf59b1b84 100644 --- a/ghost/i18n/locales/uk/portal.json +++ b/ghost/i18n/locales/uk/portal.json @@ -138,6 +138,7 @@ "Sign out": "Вихід", "Sign up": "Реєстрація", "Signup error: Invalid link": "Помилка реєстрації: недійсне посилання", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Щось пішло не так, спробуйте пізніше.", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "Вибачте, це не спрацювало.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Під час продовження підписки сталася помилка. Спробуйте ще раз.", "There was an error processing your payment. Please try again.": "Під час обробки вашого платежу сталася помилка. Спробуйте ще раз.", "There was an error sending the email, please try again": "Під час надсилання листа сталася помилка. Повторіть спробу", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Цей сайт доступний тільки за запрошенням, звернись до власника сайта для доступу.", "This site is not accepting payments at the moment.": "Цей сайт на даний момент не приймає платежі.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Забагато різних спроб входу. Повторіть спробу через {{number}} днів", "Too many different sign-in attempts, try again in {{number}} hours": "Забагато різних спроб входу. Повторіть спробу через {{number}} годин", "Too many different sign-in attempts, try again in {{number}} minutes": "Забагато різних спроб входу. Повторіть спробу через {{number}} хвилин", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Спробуйте безкоштовно протягом {{amount}} днів, надалі за {{originalPrice}}.", "Unable to initiate checkout session": "Не вдалося розпочати сеанс оформлення замовлення", "Unlock access to all newsletters by becoming a paid subscriber.": "Розблокуйте доступ до всіх розсилок, ставши платним підписником.", diff --git a/ghost/i18n/locales/ur/portal.json b/ghost/i18n/locales/ur/portal.json index 041dea35fca6..437b9b96ce86 100644 --- a/ghost/i18n/locales/ur/portal.json +++ b/ghost/i18n/locales/ur/portal.json @@ -138,6 +138,7 @@ "Sign out": "لاگ آؤٹ", "Sign up": "سائن اپ", "Signup error: Invalid link": "سائن اپ خطا: غیر معتبر لنک", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "معاف کریں، یہ کام نہیں کیا گیا۔", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "یہ سائٹ صرف دعوتی ہے، دستیابی کے لئے مالک سے رابطہ کریں۔", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "مفت ٹرائل کے لئے کوشش کریں {{amount}} دن، پھر {{originalPrice}}۔", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "ایک ادائیگی چکچکی بن کر تمام نیوزلیٹرز کا رسائی کھولیں۔", diff --git a/ghost/i18n/locales/uz/portal.json b/ghost/i18n/locales/uz/portal.json index 6e569adb5503..0dd70d28b2a7 100644 --- a/ghost/i18n/locales/uz/portal.json +++ b/ghost/i18n/locales/uz/portal.json @@ -138,6 +138,7 @@ "Sign out": "", "Sign up": "Ro'yxatdan o'tmoq", "Signup error: Invalid link": "", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Bu saytda faqat taklif qilinadi, kirish uchun egasiga murojaat qiling.", "This site is not accepting payments at the moment.": "", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", diff --git a/ghost/i18n/locales/vi/portal.json b/ghost/i18n/locales/vi/portal.json index 4c655fd91517..eea2c801481a 100644 --- a/ghost/i18n/locales/vi/portal.json +++ b/ghost/i18n/locales/vi/portal.json @@ -138,6 +138,7 @@ "Sign out": "Đăng xuất", "Sign up": "Đăng ký", "Signup error: Invalid link": "Lỗi đăng ký: Liên kết không hợp lệ", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "Xảy ra lỗi, hãy thử lại sau.", "Sorry, no recommendations are available right now.": "Rất tiếc, chưa có đề xuất nào vào lúc này.", "Sorry, that didn’t work.": "Rất tiếc, không dùng được.", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "Xảy ra lỗi khi tiếp tục gói thành viên, vui lòng thử lại", "There was an error processing your payment. Please try again.": "Xảy ra lỗi khi tiến hành thanh toán. Hãy thử lại sau.", "There was an error sending the email, please try again": "Xảy ra lỗi khi gửi email, vui lòng thử lại", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "Trang web này chỉ dành cho những người được mời, hãy liên hệ với chủ sở hữu để cấp quyền truy cập.", "This site is not accepting payments at the moment.": "Trang web này hiện chưa chấp nhận thanh toán.", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "Thử đăng nhập quá nhiều, hãy thử lại sau {{number}} ngày.", "Too many different sign-in attempts, try again in {{number}} hours": "Thử đăng nhập quá nhiều, hãy thử lại sau {{number}} giờ.", "Too many different sign-in attempts, try again in {{number}} minutes": "Thử đăng nhập quá nhiều, hãy thử lại sau {{number}} phút.", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Đọc thử {{amount}} ngày, phí sau đọc thử là {{originalPrice}}.", "Unable to initiate checkout session": "Không thể bắt đầu phiên thanh toán", "Unlock access to all newsletters by becoming a paid subscriber.": "Trở thành thành viên trả phí để mở khóa truy cập toàn bộ bản tin.", diff --git a/ghost/i18n/locales/zh-Hant/portal.json b/ghost/i18n/locales/zh-Hant/portal.json index 812f54e0b4d3..0e2b8b2de123 100644 --- a/ghost/i18n/locales/zh-Hant/portal.json +++ b/ghost/i18n/locales/zh-Hant/portal.json @@ -138,6 +138,7 @@ "Sign out": "登出", "Sign up": "註冊", "Signup error: Invalid link": "註冊錯誤:連結無效", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "伺服器錯誤,請稍後重試。", "Sorry, no recommendations are available right now.": "抱歉,目前沒有其他的推薦。", "Sorry, that didn’t work.": "抱歉,該操作無法完成。", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "續約您的訂閱時發生錯誤,請您再試一次。", "There was an error processing your payment. Please try again.": "處理您的付款時發生錯誤,請您再試一次。", "There was an error sending the email, please try again": "寄送 email 時發生錯誤,請您再試一次。", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "此網站僅限受邀請者觀看,請聯繫網站擁有者取得存取權限。", "This site is not accepting payments at the moment.": "此網站目前無付款方式。", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "嘗試過多不同的登入,請於 {{number}} 日後再試一次。", "Too many different sign-in attempts, try again in {{number}} hours": "嘗試過多不同的登入,請於 {{number}} 小時後再試一次。", "Too many different sign-in attempts, try again in {{number}} minutes": "嘗試過多不同的登入,請於 {{number}} 分鐘後再試一次。", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "免費試用 {{amount}} 天,然後以 {{originalPrice}} 開始訂閱。", "Unable to initiate checkout session": "無法建立結帳", "Unlock access to all newsletters by becoming a paid subscriber.": "成為付費會員以解鎖所有電子報內容。", diff --git a/ghost/i18n/locales/zh/portal.json b/ghost/i18n/locales/zh/portal.json index 016e303c01cc..de65aabd011b 100644 --- a/ghost/i18n/locales/zh/portal.json +++ b/ghost/i18n/locales/zh/portal.json @@ -138,6 +138,7 @@ "Sign out": "退出", "Sign up": "注册", "Signup error: Invalid link": "注册错误:链接无效", + "Signups from this email provider are not allowed": "", "Something went wrong, please try again later.": "出了点问题,请稍后再试。", "Sorry, no recommendations are available right now.": "", "Sorry, that didn’t work.": "抱歉,该操作无法完成。", @@ -165,7 +166,6 @@ "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "您的付款处理失败,请重试。", "There was an error sending the email, please try again": "", - "This email domain is not accepted, try again with a different email address": "", "This site is invite-only, contact the owner for access.": "此网站仅限邀请,联系网站所有者以获取访问", "This site is not accepting payments at the moment.": "本网站目前暂不接受付款。", "This site only accepts paid members.": "", @@ -177,6 +177,7 @@ "Too many different sign-in attempts, try again in {{number}} days": "", "Too many different sign-in attempts, try again in {{number}} hours": "", "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Too many sign-up attempts, try again later": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}}天免费试用,之后{{originalPrice}}。", "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "成为付费订阅用户以解锁全部快报。", diff --git a/ghost/magic-link/lib/MagicLink.js b/ghost/magic-link/lib/MagicLink.js index f05030a6ee0b..d0285eaca523 100644 --- a/ghost/magic-link/lib/MagicLink.js +++ b/ghost/magic-link/lib/MagicLink.js @@ -3,7 +3,7 @@ const {isEmail} = require('@tryghost/validator'); const tpl = require('@tryghost/tpl'); const messages = { invalidEmail: 'Email is not valid', - unsupportedEmailDomain: 'This email domain is not accepted, try again with a different email address' + unsupportedEmailDomain: 'Signups from this email provider are not allowed' }; /** diff --git a/ghost/magic-link/test/index.test.js b/ghost/magic-link/test/index.test.js index b7e2de246b0c..b05ebb30ef68 100644 --- a/ghost/magic-link/test/index.test.js +++ b/ghost/magic-link/test/index.test.js @@ -119,7 +119,7 @@ describe('MagicLink', function () { () => service.sendMagicLink(blockedArgs), { name: 'BadRequestError', - message: 'This email domain is not accepted, try again with a different email address' + message: 'Signups from this email provider are not allowed' } ); From c8e76fb4988b2651a4a8c3cf5a2f4b1131e3c0dd Mon Sep 17 00:00:00 2001 From: Sag Date: Wed, 22 Jan 2025 11:48:58 +0700 Subject: [PATCH 14/27] Released Portal v2.48.2 (#22041) no issue - changelog v2.48.1 -> v2.48.2: - https://github.com/TryGhost/Ghost/commit/3ca419bcbce4acf27354f269a9b3b6b311666089 --- apps/portal/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/portal/package.json b/apps/portal/package.json index 2e3d5040e8a1..46160c52c58f 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/portal", - "version": "2.48.1", + "version": "2.48.2", "license": "MIT", "repository": { "type": "git", From 3a38aef9b23c75402b9100f70a8bf4f66cc2bb99 Mon Sep 17 00:00:00 2001 From: Kevin Ansfield Date: Wed, 22 Jan 2025 07:09:08 +0000 Subject: [PATCH 15/27] Added `contentVisibilityAlpha` flag no issue - flag to allow internal testing of content visibility developments without unintentional early release to beta testers --- .../settings/advanced/labs/AlphaFeatures.tsx | 10 +++++++--- ghost/admin/app/components/koenig-lexical-editor.js | 3 ++- ghost/admin/app/services/feature.js | 1 + ghost/core/core/shared/labs.js | 3 ++- .../e2e-api/admin/__snapshots__/config.test.js.snap | 1 + 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx index 94760d8736f6..ce189d0dae1e 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/AlphaFeatures.tsx @@ -40,10 +40,14 @@ const features = [{ description: '(Highly) Experimental support for ActivityPub.', flag: 'ActivityPub' },{ - title: 'Content Visibility', - description: 'Enables content visibility in Emails', + title: 'Content Visibility (Beta)', + description: 'Enables content visibility in Emails - Changes already released to beta testers', flag: 'contentVisibility' -}, { +},{ + title: 'Content Visibility (Alpha)', + description: 'Enables content visibility in Emails - Additional changes for internal testing. NOTE: requires `contentVisibility` to also be enabled', + flag: 'contentVisibilityAlpha' +},{ title: 'Post analytics redesign', description: 'Enables redesigned Post analytics page', flag: 'postsX' diff --git a/ghost/admin/app/components/koenig-lexical-editor.js b/ghost/admin/app/components/koenig-lexical-editor.js index 490d5ac364cd..4906370f7c65 100644 --- a/ghost/admin/app/components/koenig-lexical-editor.js +++ b/ghost/admin/app/components/koenig-lexical-editor.js @@ -441,7 +441,8 @@ export default class KoenigLexicalEditor extends Component { renderLabels: !this.session.user.isContributor, feature: { collectionsCard: this.feature.collectionsCard, - contentVisibility: this.feature.contentVisibility + contentVisibility: this.feature.contentVisibility, + contentVisibilityAlpha: this.feature.contentVisibilityAlpha }, deprecated: { // todo fix typo headerV1: true // if false, shows header v1 in the menu diff --git a/ghost/admin/app/services/feature.js b/ghost/admin/app/services/feature.js index 38e4f662c92c..b5d0b9e8654c 100644 --- a/ghost/admin/app/services/feature.js +++ b/ghost/admin/app/services/feature.js @@ -74,6 +74,7 @@ export default class FeatureService extends Service { @feature('ActivityPub') ActivityPub; @feature('editorExcerpt') editorExcerpt; @feature('contentVisibility') contentVisibility; + @feature('contentVisibilityAlpha') contentVisibilityAlpha; @feature('postsX') postsX; _user = null; diff --git a/ghost/core/core/shared/labs.js b/ghost/core/core/shared/labs.js index 4ba96958bfe1..8b0d2e003090 100644 --- a/ghost/core/core/shared/labs.js +++ b/ghost/core/core/shared/labs.js @@ -51,7 +51,8 @@ const ALPHA_FEATURES = [ 'lexicalIndicators', 'adminXDemo', 'postsX', - 'captcha' + 'captcha', + 'contentVisibilityAlpha' ]; module.exports.GA_KEYS = [...GA_FEATURES]; diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap index ef614cda16d3..ad7ee1480d95 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap @@ -18,6 +18,7 @@ Object { "captcha": true, "collectionsCard": true, "contentVisibility": true, + "contentVisibilityAlpha": true, "customFonts": true, "editorExcerpt": true, "emailCustomization": true, From f07291b72cb00de591fa1b0df6c7f60eba8d5074 Mon Sep 17 00:00:00 2001 From: Sag Date: Wed, 22 Jan 2025 14:26:49 +0700 Subject: [PATCH 16/27] Added missing error message handler for the integrity token endpoint (#22043) ref https://linear.app/ghost/issue/PRO-1349 - the integrity token endpoint can return a json response with an error message (for example, when rate limited) - added the standard response handler to the integrity token endpoint in Portal, to render the error message sent by the backend --- apps/portal/src/utils/api.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/portal/src/utils/api.js b/apps/portal/src/utils/api.js index aabea5aa7a89..55bd0a8536ad 100644 --- a/apps/portal/src/utils/api.js +++ b/apps/portal/src/utils/api.js @@ -254,6 +254,10 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) { if (res.ok) { return res.text(); } else { + const humanError = await HumanReadableError.fromApiResponse(res); + if (humanError) { + throw humanError; + } throw new Error('Failed to start a members session'); } }, @@ -290,7 +294,6 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) { if (res.ok) { return 'Success'; } else { - // Try to read body error message that is human readable and should be shown to the user const humanError = await HumanReadableError.fromApiResponse(res); if (humanError) { throw humanError; From 5409ae1c68104d1edeb324b52223de43102bbe06 Mon Sep 17 00:00:00 2001 From: Sag Date: Wed, 22 Jan 2025 14:35:54 +0700 Subject: [PATCH 17/27] Released Portal v2.48.3 (#22044) no issue - changelog v2.48.2 -> v2.48.3: - https://github.com/TryGhost/Ghost/commit/f07291b72cb00de591fa1b0df6c7f60eba8d5074 --- apps/portal/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/portal/package.json b/apps/portal/package.json index 46160c52c58f..598b7dd3c476 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/portal", - "version": "2.48.2", + "version": "2.48.3", "license": "MIT", "repository": { "type": "git", From 2d0f6568fa87f48f6b1408df4622632549fc1833 Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Wed, 22 Jan 2025 17:49:11 +0000 Subject: [PATCH 18/27] Fixed reading progress indicator for very short articles (#22036) ref https://linear.app/ghost/issue/AP-653/scroll-percentage-remains-at-0percent-when-no-content-to-scroll - When an entire article fits into the viewport height, we used to show`0%` in the reading progress indicator. Now we check if that's the case, and then show `100%` if it is. --- apps/admin-x-activitypub/package.json | 2 +- .../src/components/feed/ArticleModal.tsx | 58 ++++++++++++++++--- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/apps/admin-x-activitypub/package.json b/apps/admin-x-activitypub/package.json index d5be231920a9..1d51e5499d60 100644 --- a/apps/admin-x-activitypub/package.json +++ b/apps/admin-x-activitypub/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/admin-x-activitypub", - "version": "0.3.52", + "version": "0.3.53", "license": "MIT", "repository": { "type": "git", diff --git a/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx b/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx index 3bd797faf48f..d1a2ee92cc61 100644 --- a/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx +++ b/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx @@ -18,6 +18,7 @@ import APAvatar from '../global/APAvatar'; import APReplyBox from '../global/APReplyBox'; import TableOfContents, {TOCItem} from './TableOfContents'; import getReadingTime from '../../utils/get-reading-time'; +import {useDebounce} from 'use-debounce'; interface ArticleModalProps { activityId: string; @@ -48,6 +49,7 @@ const ArticleBody: React.FC<{ fontFamily: SelectOption; onHeadingsExtracted?: (headings: TOCItem[]) => void; onIframeLoad?: (iframe: HTMLIFrameElement) => void; + onLoadingChange?: (isLoading: boolean) => void; }> = ({ heading, image, @@ -57,7 +59,8 @@ const ArticleBody: React.FC<{ lineHeight, fontFamily, onHeadingsExtracted, - onIframeLoad + onIframeLoad, + onLoadingChange }) => { const site = useBrowseSite(); const siteData = site.data?.site; @@ -213,7 +216,6 @@ const ArticleBody: React.FC<{ if (iframeWindow && typeof iframeWindow.resizeIframe === 'function') { iframeWindow.resizeIframe(); } else { - // Fallback: trigger a resize event const resizeEvent = new Event('resize'); iframeDocument.dispatchEvent(resizeEvent); } @@ -269,6 +271,11 @@ const ArticleBody: React.FC<{ return () => iframe.removeEventListener('load', handleLoad); }, [onHeadingsExtracted, onIframeLoad]); + // Update parent when loading state changes + useEffect(() => { + onLoadingChange?.(isLoading); + }, [isLoading, onLoadingChange]); + return (
    @@ -526,30 +533,66 @@ const ArticleModal: React.FC = ({ const currentGridWidth = `${parseInt(currentMaxWidth) - 64}px`; const [readingProgress, setReadingProgress] = useState(0); + const [isLoading, setIsLoading] = useState(true); + + // Add debounced version of setReadingProgress + const [debouncedSetReadingProgress] = useDebounce(setReadingProgress, 100); + + const PROGRESS_INCREMENT = 5; // Progress is shown in 5% increments (0%, 5%, 10%, etc.) useEffect(() => { const container = document.querySelector('.overflow-y-auto'); const article = document.getElementById('object-content'); const handleScroll = () => { + if (isLoading) { + return; + } + if (!container || !article) { return; } const articleRect = article.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); + + const isContentShorterThanViewport = articleRect.height <= containerRect.height; + + if (isContentShorterThanViewport) { + debouncedSetReadingProgress(100); + return; + } + const scrolledPast = Math.max(0, containerRect.top - articleRect.top); const totalHeight = (article as HTMLElement).offsetHeight - (container as HTMLElement).offsetHeight; const rawProgress = Math.min(Math.max((scrolledPast / totalHeight) * 100, 0), 100); - const progress = Math.round(rawProgress / 5) * 5; + const progress = Math.round(rawProgress / PROGRESS_INCREMENT) * PROGRESS_INCREMENT; - setReadingProgress(progress); + debouncedSetReadingProgress(progress); }; + if (isLoading) { + return; + } + + const observer = new MutationObserver(handleScroll); + if (article) { + observer.observe(article, { + childList: true, + subtree: true, + characterData: true + }); + } + container?.addEventListener('scroll', handleScroll); - return () => container?.removeEventListener('scroll', handleScroll); - }, []); + handleScroll(); + + return () => { + container?.removeEventListener('scroll', handleScroll); + observer.disconnect(); + }; + }, [isLoading, debouncedSetReadingProgress]); const [tocItems, setTocItems] = useState([]); const [activeHeadingId, setActiveHeadingId] = useState(null); @@ -575,7 +618,6 @@ const ArticleModal: React.FC = ({ return; } - // Use offsetTop for absolute position within the document const headingOffset = heading.offsetTop; container.scrollTo({ @@ -602,7 +644,6 @@ const ArticleModal: React.FC = ({ return; } - // Get all heading elements and their positions const headings = tocItems .map(item => doc.getElementById(item.id)) .filter((el): el is HTMLElement => el !== null) @@ -845,6 +886,7 @@ const ArticleModal: React.FC = ({ lineHeight={LINE_HEIGHTS[currentLineHeightIndex]} onHeadingsExtracted={handleHeadingsExtracted} onIframeLoad={handleIframeLoad} + onLoadingChange={setIsLoading} />
    Date: Thu, 23 Jan 2025 08:21:37 +0700 Subject: [PATCH 19/27] Added missing Vietnamese translation for portal (#21948) Translating "This site only accepts paid members." Co-authored-by: Chris Raible --- ghost/i18n/locales/vi/portal.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ghost/i18n/locales/vi/portal.json b/ghost/i18n/locales/vi/portal.json index eea2c801481a..231ab2b5dcbb 100644 --- a/ghost/i18n/locales/vi/portal.json +++ b/ghost/i18n/locales/vi/portal.json @@ -168,7 +168,7 @@ "There was an error sending the email, please try again": "Xảy ra lỗi khi gửi email, vui lòng thử lại", "This site is invite-only, contact the owner for access.": "Trang web này chỉ dành cho những người được mời, hãy liên hệ với chủ sở hữu để cấp quyền truy cập.", "This site is not accepting payments at the moment.": "Trang web này hiện chưa chấp nhận thanh toán.", - "This site only accepts paid members.": "", + "This site only accepts paid members.": "Trang web này chỉ chấp nhận người dùng trả phí.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Để hoàn tất đăng ký, nhấn vào liên kết xác nhận được gửi tới email của bạn. Sau 3 phút mà không thấy, hãy kiểm tra hộp thư spam!", "To continue to stay up to date, subscribe to {{publication}} below.": "Để tiếp tục được cập nhật, hãy đăng ký {{publication}} bên dưới.", "Too many attempts try again in {{number}} days.": "Thử quá nhiều, hãy thử lại sau {{number}} ngày.", From 568322c378b762bf695d6c3e1e6fa7fd11b5e757 Mon Sep 17 00:00:00 2001 From: Sag Date: Thu, 23 Jan 2025 11:12:29 +0700 Subject: [PATCH 20/27] Added new setting in the database: blocked_email_domains [migration] (#22046) ref https://linear.app/ghost/issue/ENG-1973 ref https://app.incident.io/ghost/incidents/132 - added a new database setting: `blocked_email_domains` (array, default: `[]`) - this setting will allow publishers to block additional email domains during member signups, on top of the ones blocklisted at a config level (follow-up PR) --- .../api/endpoints/utils/serializers/input/settings.js | 3 ++- ...25-01-23-02-51-10-add-blocked-email-domains-setting.js | 8 ++++++++ .../data/schema/default-settings/default-settings.json | 4 ++++ ghost/core/test/unit/server/data/exporter/index.test.js | 2 +- ghost/core/test/unit/server/data/schema/integrity.test.js | 2 +- 5 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 ghost/core/core/server/data/migrations/versions/5.108/2025-01-23-02-51-10-add-blocked-email-domains-setting.js diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js b/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js index 4bda40ceda4e..709e41adfc4e 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js @@ -74,7 +74,8 @@ const EDITABLE_SETTINGS = [ 'donations_suggested_amount', 'recommendations_enabled', 'body_font', - 'heading_font' + 'heading_font', + 'blocked_email_domains' ]; module.exports = { diff --git a/ghost/core/core/server/data/migrations/versions/5.108/2025-01-23-02-51-10-add-blocked-email-domains-setting.js b/ghost/core/core/server/data/migrations/versions/5.108/2025-01-23-02-51-10-add-blocked-email-domains-setting.js new file mode 100644 index 000000000000..2404d4b9cab3 --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/5.108/2025-01-23-02-51-10-add-blocked-email-domains-setting.js @@ -0,0 +1,8 @@ +const {addSetting} = require('../../utils'); + +module.exports = addSetting({ + key: 'blocked_email_domains', + value: '[]', + type: 'array', + group: 'members' +}); diff --git a/ghost/core/core/server/data/schema/default-settings/default-settings.json b/ghost/core/core/server/data/schema/default-settings/default-settings.json index 2d036a745c28..7094706acd48 100644 --- a/ghost/core/core/server/data/schema/default-settings/default-settings.json +++ b/ghost/core/core/server/data/schema/default-settings/default-settings.json @@ -315,6 +315,10 @@ "isIn": [["true", "false"]] }, "type": "boolean" + }, + "blocked_email_domains": { + "defaultValue": "[]", + "type": "array" } }, "portal": { diff --git a/ghost/core/test/unit/server/data/exporter/index.test.js b/ghost/core/test/unit/server/data/exporter/index.test.js index 40bcae2e4e01..0a790cf61211 100644 --- a/ghost/core/test/unit/server/data/exporter/index.test.js +++ b/ghost/core/test/unit/server/data/exporter/index.test.js @@ -236,7 +236,7 @@ describe('Exporter', function () { // NOTE: if default settings changed either modify the settings keys blocklist or increase allowedKeysLength // This is a reminder to think about the importer/exporter scenarios ;) - const allowedKeysLength = 88; + const allowedKeysLength = 89; totalKeysLength.should.eql(SETTING_KEYS_BLOCKLIST.length + allowedKeysLength); }); }); diff --git a/ghost/core/test/unit/server/data/schema/integrity.test.js b/ghost/core/test/unit/server/data/schema/integrity.test.js index c05139ed5de8..9485f954938a 100644 --- a/ghost/core/test/unit/server/data/schema/integrity.test.js +++ b/ghost/core/test/unit/server/data/schema/integrity.test.js @@ -37,7 +37,7 @@ describe('DB version integrity', function () { // Only these variables should need updating const currentSchemaHash = 'b26690fb57ffd0edbddb4cd9e02b17d6'; const currentFixturesHash = '80e79d1efd5da275e19cb375afb4ad04'; - const currentSettingsHash = '80387fdbda0102ab4995660d5d98007c'; + const currentSettingsHash = '05366d793079c93b87477ec0404301c6'; const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01'; // If this test is failing, then it is likely a change has been made that requires a DB version bump, From e41fc2c4d581764b9707ccf5d8b2ea5f765305ab Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Thu, 23 Jan 2025 08:22:04 +0100 Subject: [PATCH 21/27] Shade updates (#22045) ref https://linear.app/ghost/issue/DES-1085/update-shade-to-be-used-in-activitypub - Shade so far was just used in our playground (Post analytics). It needed to be prepared so that it can be integrated in real projects like ActivityPub. This means cleaning up everything related to it like conventions, file structure, documentation etc. --- apps/shade/src/boilerplate.stories.tsx | 18 --- apps/shade/src/boilerplate.tsx | 15 -- .../src/components/ui/avatar.stories.tsx | 35 +++++ .../shade/src/components/ui/badge.stories.tsx | 17 +++ .../shade/src/components/ui/chart.stories.tsx | 102 +++++++++++++ .../src/components/ui/dialog.stories.tsx | 34 +++++ apps/shade/src/components/ui/icon.ts | 3 +- .../src/components/ui/separator.stories.tsx | 15 ++ .../shade/src/components/ui/table.stories.tsx | 50 +++++++ .../src/components/ui/tooltip.stories.tsx | 37 +++++ apps/shade/src/components/ui/tooltip.tsx | 20 +-- apps/shade/src/docs/Conventions.mdx | 22 ++- apps/shade/src/docs/CreatingComponents.mdx | 26 +++- apps/shade/src/docs/Environment.mdx | 4 +- apps/shade/src/docs/UsingComponents.mdx | 2 +- apps/shade/src/docs/Welcome.mdx | 8 +- apps/shade/src/docs/assets/tech-stack.png | Bin 0 -> 117912 bytes ...tyState.tsx => use-global-dirty-state.tsx} | 0 apps/shade/src/index.ts | 6 +- apps/shade/src/lib/utils.ts | 136 ++++++++++++++++++ apps/shade/src/providers/ShadeProvider.tsx | 2 +- apps/shade/src/utils/debounce.ts | 24 ---- apps/shade/src/utils/formatText.ts | 6 - apps/shade/src/utils/formatUrl.ts | 100 ------------- apps/shade/test/unit/utils/formatUrl.test.ts | 2 +- apps/shade/tsconfig.json | 4 +- 26 files changed, 497 insertions(+), 191 deletions(-) delete mode 100644 apps/shade/src/boilerplate.stories.tsx delete mode 100644 apps/shade/src/boilerplate.tsx create mode 100644 apps/shade/src/components/ui/avatar.stories.tsx create mode 100644 apps/shade/src/components/ui/badge.stories.tsx create mode 100644 apps/shade/src/components/ui/chart.stories.tsx create mode 100644 apps/shade/src/components/ui/dialog.stories.tsx create mode 100644 apps/shade/src/components/ui/separator.stories.tsx create mode 100644 apps/shade/src/components/ui/table.stories.tsx create mode 100644 apps/shade/src/components/ui/tooltip.stories.tsx create mode 100644 apps/shade/src/docs/assets/tech-stack.png rename apps/shade/src/hooks/{useGlobalDirtyState.tsx => use-global-dirty-state.tsx} (100%) delete mode 100644 apps/shade/src/utils/debounce.ts delete mode 100644 apps/shade/src/utils/formatText.ts delete mode 100644 apps/shade/src/utils/formatUrl.ts diff --git a/apps/shade/src/boilerplate.stories.tsx b/apps/shade/src/boilerplate.stories.tsx deleted file mode 100644 index 37064139442e..000000000000 --- a/apps/shade/src/boilerplate.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type {Meta, StoryObj} from '@storybook/react'; - -import BoilerPlate from './boilerplate'; - -const meta = { - title: 'Meta / Boilerplate', - component: BoilerPlate, - tags: ['autodocs'] -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - children: 'This is a boilerplate component. Use as a basis to create new components.' - } -}; diff --git a/apps/shade/src/boilerplate.tsx b/apps/shade/src/boilerplate.tsx deleted file mode 100644 index 6aa687cc9d44..000000000000 --- a/apps/shade/src/boilerplate.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; - -interface BoilerPlateProps { - children?: React.ReactNode; -} - -const BoilerPlate: React.FC = ({children}) => { - return ( - <> - {children} - - ); -}; - -export default BoilerPlate; diff --git a/apps/shade/src/components/ui/avatar.stories.tsx b/apps/shade/src/components/ui/avatar.stories.tsx new file mode 100644 index 000000000000..9cacd9262ba4 --- /dev/null +++ b/apps/shade/src/components/ui/avatar.stories.tsx @@ -0,0 +1,35 @@ +import type {Meta, StoryObj} from '@storybook/react'; +import {Avatar, AvatarFallback, AvatarImage} from './avatar'; + +const meta = { + title: 'Components / Avatar', + component: Avatar, + tags: ['autodocs'], + argTypes: { + children: { + table: { + disable: true + } + } + } +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: AG + } +}; + +export const WithImage: Story = { + args: { + children: ( + <> + + AG + + ) + } +}; diff --git a/apps/shade/src/components/ui/badge.stories.tsx b/apps/shade/src/components/ui/badge.stories.tsx new file mode 100644 index 000000000000..65e1bf9b0e4d --- /dev/null +++ b/apps/shade/src/components/ui/badge.stories.tsx @@ -0,0 +1,17 @@ +import type {Meta, StoryObj} from '@storybook/react'; +import {Badge} from './badge'; + +const meta = { + title: 'Components / Badge', + component: Badge, + tags: ['autodocs'] +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'Badge' + } +}; diff --git a/apps/shade/src/components/ui/chart.stories.tsx b/apps/shade/src/components/ui/chart.stories.tsx new file mode 100644 index 000000000000..d70fdaaa9c7c --- /dev/null +++ b/apps/shade/src/components/ui/chart.stories.tsx @@ -0,0 +1,102 @@ +import type {Meta} from '@storybook/react'; +import {ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent} from './chart'; +import React from 'react'; +import {Label, Pie, PieChart} from 'recharts'; + +const meta = { + title: 'Components / Charts', + component: ChartContainer, + tags: ['autodocs'], + argTypes: { + children: { + control: false + } + } +} satisfies Meta; + +export default meta; + +export const Default = { + render: function ChartStory() { + const chartData = React.useMemo(() => { + return [ + {browser: 'chrome', visitors: 98, fill: 'var(--color-chrome)'}, + {browser: 'safari', visitors: 17, fill: 'var(--color-safari)'} + ]; + }, []); + + const chartConfig = { + visitors: { + label: 'Reactions' + }, + chrome: { + label: 'More like this', + color: 'hsl(var(--chart-1))' + }, + safari: { + label: 'Less like this', + color: 'hsl(var(--chart-5))' + } + } satisfies ChartConfig; + + const totalVisitors = React.useMemo(() => { + return chartData.reduce((acc, curr) => acc + curr.visitors, 0); + }, [chartData]); + + return ( + <> + + + } + cursor={false} + /> + + + + +
    + Visit ShadCN/UI Charts docs for usage details. +
    + + ); + } +}; \ No newline at end of file diff --git a/apps/shade/src/components/ui/dialog.stories.tsx b/apps/shade/src/components/ui/dialog.stories.tsx new file mode 100644 index 000000000000..4a4727b8fce1 --- /dev/null +++ b/apps/shade/src/components/ui/dialog.stories.tsx @@ -0,0 +1,34 @@ +import type {Meta, StoryObj} from '@storybook/react'; +import {Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle} from './dialog'; +import {Button} from './button'; + +const meta = { + title: 'Components / Dialog', + component: Dialog, + tags: ['autodocs'], + argTypes: { + children: { + table: { + disable: true + } + } + } +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: ( + <> + + + + Are you absolutely sure? + + + + ) + } +}; diff --git a/apps/shade/src/components/ui/icon.ts b/apps/shade/src/components/ui/icon.ts index 6d76addedd82..c08f51561015 100644 --- a/apps/shade/src/components/ui/icon.ts +++ b/apps/shade/src/components/ui/icon.ts @@ -1,7 +1,6 @@ import React from 'react'; import {cva, type VariantProps} from 'class-variance-authority'; -import {cn} from '@/lib/utils'; -import {kebabToPascalCase} from '@/utils/formatText'; +import {cn, kebabToPascalCase} from '@/lib/utils'; const iconVariants = cva('', { variants: { diff --git a/apps/shade/src/components/ui/separator.stories.tsx b/apps/shade/src/components/ui/separator.stories.tsx new file mode 100644 index 000000000000..5708b376f439 --- /dev/null +++ b/apps/shade/src/components/ui/separator.stories.tsx @@ -0,0 +1,15 @@ +import type {Meta, StoryObj} from '@storybook/react'; +import {Separator} from './separator'; + +const meta = { + title: 'Components / Separator', + component: Separator, + tags: ['autodocs'] +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {} +}; diff --git a/apps/shade/src/components/ui/table.stories.tsx b/apps/shade/src/components/ui/table.stories.tsx new file mode 100644 index 000000000000..83915c8adf6c --- /dev/null +++ b/apps/shade/src/components/ui/table.stories.tsx @@ -0,0 +1,50 @@ +import type {Meta, StoryObj} from '@storybook/react'; +import {Table, TableCaption, TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell} from './table'; + +const meta = { + title: 'Components / Table', + component: Table, + tags: ['autodocs'], + argTypes: { + children: { + table: { + disable: true + } + } + } +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: ( + <> + A list of your recent invoices. + + + Invoice + Status + Method + Amount + + + + + ABC-123 + Paid + Card + $2,500.00 + + + + + Total + $2,500.00 + + + + ) + } +}; diff --git a/apps/shade/src/components/ui/tooltip.stories.tsx b/apps/shade/src/components/ui/tooltip.stories.tsx new file mode 100644 index 000000000000..ef9e0d3e5bfb --- /dev/null +++ b/apps/shade/src/components/ui/tooltip.stories.tsx @@ -0,0 +1,37 @@ +import type {Meta, StoryObj} from '@storybook/react'; +import {Tooltip, TooltipTrigger, TooltipContent} from './tooltip'; +import {TooltipProvider} from '@radix-ui/react-tooltip'; + +const meta = { + title: 'Components / Tooltip', + component: Tooltip, + tags: ['autodocs'], + decorators: [ + Story => ( + + + + ) + ], + argTypes: { + children: { + table: { + disable: true + } + } + } +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: ( + <> + Hover me + Tooltip content + + ) + } +}; diff --git a/apps/shade/src/components/ui/tooltip.tsx b/apps/shade/src/components/ui/tooltip.tsx index 7bdc8a9edd5b..5ac524a159b8 100644 --- a/apps/shade/src/components/ui/tooltip.tsx +++ b/apps/shade/src/components/ui/tooltip.tsx @@ -14,15 +14,17 @@ const TooltipContent = React.forwardRef< React.ComponentPropsWithoutRef >(({className, sideOffset = 4, ...props}, ref) => ( - +
    + +
    )); TooltipContent.displayName = TooltipPrimitive.Content.displayName; diff --git a/apps/shade/src/docs/Conventions.mdx b/apps/shade/src/docs/Conventions.mdx index b34dd603ef85..0796dcd032f6 100644 --- a/apps/shade/src/docs/Conventions.mdx +++ b/apps/shade/src/docs/Conventions.mdx @@ -26,6 +26,26 @@ className={cn(buttonVariants({variant, size, className}))} ## TailwindCSS color naming -[TK] +For gray colors up until now we've used `grey`. ShadCN follows TailwindCSS naming conventions and uses `gray` when you install a component. We also decided to go with this mainly to avoid having to manually override color names and risking manual errors in the design system. + +## Filenames + +Our generic naming conventions is: + +- `PascalCase` for React component names +- `kebab-case` for non-React-components like utilities, hooks and so on +- `camelCase` for functions, objects, types etc. + +When you install a ShadCN component via the CLI it'll create files with kebab-case. This is _not_ following our standards. Unfortunately changing _only_ filename casing in MacOS and Github is a **massive** PITA so for now we're accepting this inconsistency and let ShadCN create its files as is. + +## File structure + +- `/src/components/` — Root directory for components + - `/src/components/ui/` — Directory for atomic UI components (foundational UI elements such as buttons, dropdowns etc.) + - `/src/components/layout` — Complex, globally reusable components (such as a page or header container etc.) +- `/src/hooks/` — Custom Reach hooks +- `/src/providers/` — Context providers +- `/src/ib/utils.ts` — Utilities +- `/src/docs/` — Shade documentation
    diff --git a/apps/shade/src/docs/CreatingComponents.mdx b/apps/shade/src/docs/CreatingComponents.mdx index 183fb83fc725..a852cdbf08fa 100644 --- a/apps/shade/src/docs/CreatingComponents.mdx +++ b/apps/shade/src/docs/CreatingComponents.mdx @@ -6,12 +6,18 @@ import { Meta } from '@storybook/blocks'; # Adding new components -ShadCN/UI is actually not a library, it's a starting point — this means that whenever you install a new ShadCN/UI component it adds a new React `tsx` file with the default implementation for the given UI component. Then you can customize the implementation by applying TW classes, creating new variants and so on. +

    ShadCN/UI is not a library, it's a starting point — this means that whenever you install a new ShadCN/UI component it adds new React component(s) with the default implementation for the given UI component. Then you can customize the implementation by applying TW classes, creating new variants and so on.

    As an example, let's see how to add a "Button" component. +--- + **Step 1:** Go to [ShadCN/UI component list](https://ui.shadcn.com/docs/components) and find **Button** +> ⚠️ Sometimes the npx command fails with a massive error. This usually happens if ShadCN tries to reinstall an already existing third party library or component (e.g. `radix-ui/dialog` or `lucide-react`). In these cases, find out which package causes the issue (usually it's the one indicated under the manual installation of the given component), remove it from Shade's `package.json` and retry adding the ShadCN component via the CLI command. + +--- + **Step 2:** Install the component In the `shade` folder follow the ShadCN/UI CLI install instructions: @@ -35,6 +41,8 @@ If you take a look at the source of this component you'll see that the whole but - You can't "update" ShadCN/UI components, you can only reinstall. This means that if you re-run the install command it overwrites the existing implementation including all your changes, so be careful. - You can change the implementation as you wish. You can add, remove or CSS classes, create new variants and add UI logic if you want. +--- + **Step 3:** Create stories for the new component In order to make the new component appear in Storybook, you need to create at least one story for it. You can use the Boilerplate story as a starting point: copy it as `[component].stories.tsx` to the same folder as your component: @@ -53,6 +61,8 @@ For a story, at a minimum you'll need to: - Update the imported component (for the button example: `import {Button} from './button'`), and update all references in the stories file - Change the meta data (most imporantly, `title: 'Components / Button'`, where "Components" will be the parent directory of Button in Storybook — this is a meta directory it doesn't have to exist in the file system) +--- + **Step 4:** (Optional) Add or remove component variants ShadCN/UI uses [Class Variance Authority](https://cva.style/docs) to manage variants for UI components. If you look at the Button component, you'll see an example implementation of this: @@ -83,6 +93,8 @@ export interface ButtonProps } ``` +--- + **Step 5:** Export the component In order to be able to import the new component in apps, you'll need to export it in the `index.ts` file of Shade, like this: @@ -93,6 +105,12 @@ export * from './components/ui/button'; That's it — you've just added a new component to Shade and made it ready to use in other React apps. +--- + +**Note:** ShadCN uses a few third party libraries. Since we export everything from Shade under the project `@tryghost/shade` there might be conflicts if a third party library uses similar component names as some other components in Shade. For example, ShadCN uses Recharts to display charts. Recharts has a `` component and Shade also has one. To overcome this issue we alias all third party exports (e.g. `export * as Recharts from "recharts"`). + +--- + ## Stories With the automatic variant method, most of the _style related_ documentation is handled automatically in Shade. New stories should be created for non-stylistic use cases which are imporant to be able check. For the Button component an example could be related to the contents of the button: text-only, icon-only and icon-text buttons should be separated into different stories. @@ -113,6 +131,10 @@ The `components.json` file contains the ShadCN/UI default configuration, so when ## Custom components -[TK] +It is of course possible to create our own components. Examine how ShadCN components are structured and follow their patterns: + +- Create a single file for each UI component +- Create [composable components](https://blog.tomaszgil.me/choosing-the-right-path-composable-vs-configurable-components-in-react) (multiple React components), _not_ configurable ones (lots of props). Create all corresponding React components in the same file (take a look at the Dropdown Menu implementation for an example). +- Export custom components the same way as you would with ShadCN ones.
    diff --git a/apps/shade/src/docs/Environment.mdx b/apps/shade/src/docs/Environment.mdx index 4a0551f52691..1b826dac0af4 100644 --- a/apps/shade/src/docs/Environment.mdx +++ b/apps/shade/src/docs/Environment.mdx @@ -13,7 +13,7 @@ In order to make the proper TailwindCSS classes appear in code completion, you n Then, verify your VSCode settings: -1. Open settings.json (Cmd+Shift+P, then "Preferences: Open Settings (JSON)") +1. Open `settings.json` (Cmd+Shift+P, then "Preferences: Open Settings (JSON)") 2. Add these settings: ``` @@ -26,7 +26,7 @@ Then, verify your VSCode settings: } ``` -Also, ensure your project has a postcss.config.js file: +Also, ensure your project has a `postcss.config.js` file: ``` module.exports = { diff --git a/apps/shade/src/docs/UsingComponents.mdx b/apps/shade/src/docs/UsingComponents.mdx index 66e48ef34779..cfef2089a0ed 100644 --- a/apps/shade/src/docs/UsingComponents.mdx +++ b/apps/shade/src/docs/UsingComponents.mdx @@ -6,7 +6,7 @@ import { Meta } from '@storybook/blocks'; # Component usage -The simplest way to use a component is to select the variant in Storybook and copy + paste the code from it. All components have their `className` prop forwarded to their rendered HTML component, which allows you to override any default classes. +

    The simplest way to use a component is to select the variant in Storybook and copy + paste the code from it. All components have their `className` prop forwarded to their rendered HTML component, which allows you to override any default classes.

    ## UI component API's diff --git a/apps/shade/src/docs/Welcome.mdx b/apps/shade/src/docs/Welcome.mdx index 95182399dd42..c5598da49112 100644 --- a/apps/shade/src/docs/Welcome.mdx +++ b/apps/shade/src/docs/Welcome.mdx @@ -1,4 +1,5 @@ import { Meta } from '@storybook/blocks'; +import techStackImage from './assets/tech-stack.png'; @@ -8,9 +9,10 @@ import { Meta } from '@storybook/blocks';

    **Shade** is Ghost's design system for product design. It contains all the necessary components and their usage that help you design a great product experience.

    -## Stack - Shade uses the RadixUI based open source UI library, [ShadCN/UI](https://ui.shadcn.com/) as its foundation which does the heavy lifting and gives a great starting point for any UI component. Additionally Shade is built using [TailwindCSS](https://www.tailwindcss.com). Before diving deep into working with Shade, please get familiar with both of these libraries. -Shade uses [Storybook](https://storybook.js.org/) to document components and best practices. +Technology Stack + +Shade uses [Storybook](https://storybook.js.org/) to test and document components and best practices. For every component used in production, there must be a Storybook entry with at least one story that showcases the component. +
    diff --git a/apps/shade/src/docs/assets/tech-stack.png b/apps/shade/src/docs/assets/tech-stack.png new file mode 100644 index 0000000000000000000000000000000000000000..7bf53082fdcb79100b23036ec8b345157ed51fe4 GIT binary patch literal 117912 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 literal 0 HcmV?d00001 diff --git a/apps/shade/src/hooks/useGlobalDirtyState.tsx b/apps/shade/src/hooks/use-global-dirty-state.tsx similarity index 100% rename from apps/shade/src/hooks/useGlobalDirtyState.tsx rename to apps/shade/src/hooks/use-global-dirty-state.tsx diff --git a/apps/shade/src/index.ts b/apps/shade/src/index.ts index dab7abfff2a9..f45cd4a989f9 100644 --- a/apps/shade/src/index.ts +++ b/apps/shade/src/index.ts @@ -35,13 +35,11 @@ export {ReactComponent as GoogleLogo} from './assets/images/google-logo.svg'; export {ReactComponent as TwitterLogo} from './assets/images/twitter-logo.svg'; export {ReactComponent as XLogo} from './assets/images/x-logo.svg'; -export {default as useGlobalDirtyState} from './hooks/useGlobalDirtyState'; +export {default as useGlobalDirtyState} from './hooks/use-global-dirty-state'; // Utils export * from '@/lib/utils'; -export {cn} from '@/lib/utils'; -export {debounce} from './utils/debounce'; -export {formatUrl} from './utils/formatUrl'; +export {cn, debounce, kebabToPascalCase, formatUrl} from '@/lib/utils'; export {default as ShadeApp} from './ShadeApp'; export type {ShadeAppProps} from './ShadeApp'; diff --git a/apps/shade/src/lib/utils.ts b/apps/shade/src/lib/utils.ts index 45b87de67cde..d526eeed3713 100644 --- a/apps/shade/src/lib/utils.ts +++ b/apps/shade/src/lib/utils.ts @@ -1,6 +1,142 @@ import {clsx, type ClassValue} from 'clsx'; +import isEmail from 'validator/es/lib/isEmail'; import {twMerge} from 'tailwind-merge'; +// Helper to merge Tailwind classes export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +// Helper to debounce a function +export function debounce(func: (...args: T) => void, wait: number, immediate: boolean = false): (...args: T) => void { + let timeoutId: ReturnType | null; + + return function (this: unknown, ...args: T): void { + const later = () => { + timeoutId = null; + if (!immediate) { + func.apply(this, args); + } + }; + + const callNow = immediate && !timeoutId; + + if (timeoutId) { + clearTimeout(timeoutId); + } + + timeoutId = setTimeout(later, wait); + + if (callNow) { + func.apply(this, args); + } + }; +} + +// Helper to convert kebab-case to PascalCase with numbers +export const kebabToPascalCase = (str: string): string => { + const processed = str + .replace(/[-_]([a-z0-9])/gi, (_, char) => char.toUpperCase()); + return processed.charAt(0).toUpperCase() + processed.slice(1); +}; + +// Helper to format a URL +export const formatUrl = (value: string, baseUrl?: string, nullable?: boolean) => { + if (nullable && !value) { + return {save: null, display: ''}; + } + + let url = value.trim(); + + if (!url) { + if (baseUrl) { + return {save: '/', display: baseUrl}; + } + return {save: '', display: ''}; + } + + // if we have an email address, add the mailto: + if (isEmail(url)) { + return {save: `mailto:${url}`, display: `mailto:${url}`}; + } + + const isAnchorLink = url.match(/^#/); + if (isAnchorLink) { + return {save: url, display: url}; + } + + const isProtocolRelative = url.match(/^(\/\/)/); + if (isProtocolRelative) { + return {save: url, display: url}; + } + + if (!baseUrl) { + // Absolute URL with no base URL + if (!url.startsWith('http')) { + url = `https://${url}`; + } + } + + // If it doesn't look like a URL, leave it as is rather than assuming it's a pathname etc + if (!url.match(/^[a-zA-Z0-9-]+:/) && !url.match(/^(\/|\?)/)) { + return {save: url, display: url}; + } + + let parsedUrl: URL; + + try { + parsedUrl = new URL(url, baseUrl); + } catch (e) { + return {save: url, display: url}; + } + + if (!baseUrl) { + return {save: parsedUrl.toString(), display: parsedUrl.toString()}; + } + const parsedBaseUrl = new URL(baseUrl); + + let isRelativeToBasePath = parsedUrl.pathname && parsedUrl.pathname.indexOf(parsedBaseUrl.pathname) === 0; + + // if our path is only missing a trailing / mark it as relative + if (`${parsedUrl.pathname}/` === parsedBaseUrl.pathname) { + isRelativeToBasePath = true; + } + + const isOnSameHost = parsedUrl.host === parsedBaseUrl.host; + + // if relative to baseUrl, remove the base url before sending to action + if (isOnSameHost && isRelativeToBasePath) { + url = url.replace(/^[a-zA-Z0-9-]+:/, ''); + url = url.replace(/^\/\//, ''); + url = url.replace(parsedBaseUrl.host, ''); + url = url.replace(parsedBaseUrl.pathname, ''); + + if (!url.match(/^\//)) { + url = `/${url}`; + } + } + + if (!url.match(/\/$/) && !url.match(/[.#?]/)) { + url = `${url}/`; + } + + // we update with the relative URL but then transform it back to absolute + // for the input value. This avoids problems where the underlying relative + // value hasn't changed even though the input value has + return {save: url, display: displayFromBase(url, baseUrl)}; +}; + +// Helper to display a URL from a base URL +const displayFromBase = (url: string, baseUrl: string) => { + // Ensure base url has a trailing slash + if (!baseUrl.endsWith('/')) { + baseUrl += '/'; + } + + // Remove leading slash from url + if (url.startsWith('/')) { + url = url.substring(1); + } + + return new URL(url, baseUrl).toString(); +}; diff --git a/apps/shade/src/providers/ShadeProvider.tsx b/apps/shade/src/providers/ShadeProvider.tsx index 3d50e0558c77..49d03a416aeb 100644 --- a/apps/shade/src/providers/ShadeProvider.tsx +++ b/apps/shade/src/providers/ShadeProvider.tsx @@ -2,7 +2,7 @@ import NiceModal from '@ebay/nice-modal-react'; import React, {createContext, useContext, useState} from 'react'; import {Toaster} from 'react-hot-toast'; // import {FetchKoenigLexical} from '../global/form/HtmlEditor'; -import {GlobalDirtyStateProvider} from '../hooks/useGlobalDirtyState'; +import {GlobalDirtyStateProvider} from '../hooks/use-global-dirty-state'; interface ShadeContextType { isAnyTextFieldFocused: boolean; diff --git a/apps/shade/src/utils/debounce.ts b/apps/shade/src/utils/debounce.ts deleted file mode 100644 index add7c3d5f160..000000000000 --- a/apps/shade/src/utils/debounce.ts +++ /dev/null @@ -1,24 +0,0 @@ -export function debounce(func: (...args: T) => void, wait: number, immediate: boolean = false): (...args: T) => void { - let timeoutId: ReturnType | null; - - return function (this: unknown, ...args: T): void { - const later = () => { - timeoutId = null; - if (!immediate) { - func.apply(this, args); - } - }; - - const callNow = immediate && !timeoutId; - - if (timeoutId) { - clearTimeout(timeoutId); - } - - timeoutId = setTimeout(later, wait); - - if (callNow) { - func.apply(this, args); - } - }; -} diff --git a/apps/shade/src/utils/formatText.ts b/apps/shade/src/utils/formatText.ts deleted file mode 100644 index 9b761f9c4182..000000000000 --- a/apps/shade/src/utils/formatText.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Helper to convert kebab-case to PascalCase with numbers -export const kebabToPascalCase = (str: string): string => { - const processed = str - .replace(/[-_]([a-z0-9])/gi, (_, char) => char.toUpperCase()); - return processed.charAt(0).toUpperCase() + processed.slice(1); -}; diff --git a/apps/shade/src/utils/formatUrl.ts b/apps/shade/src/utils/formatUrl.ts deleted file mode 100644 index b05852be6c31..000000000000 --- a/apps/shade/src/utils/formatUrl.ts +++ /dev/null @@ -1,100 +0,0 @@ -import isEmail from 'validator/es/lib/isEmail'; - -export const formatUrl = (value: string, baseUrl?: string, nullable?: boolean) => { - if (nullable && !value) { - return {save: null, display: ''}; - } - - let url = value.trim(); - - if (!url) { - if (baseUrl) { - return {save: '/', display: baseUrl}; - } - return {save: '', display: ''}; - } - - // if we have an email address, add the mailto: - if (isEmail(url)) { - return {save: `mailto:${url}`, display: `mailto:${url}`}; - } - - const isAnchorLink = url.match(/^#/); - if (isAnchorLink) { - return {save: url, display: url}; - } - - const isProtocolRelative = url.match(/^(\/\/)/); - if (isProtocolRelative) { - return {save: url, display: url}; - } - - if (!baseUrl) { - // Absolute URL with no base URL - if (!url.startsWith('http')) { - url = `https://${url}`; - } - } - - // If it doesn't look like a URL, leave it as is rather than assuming it's a pathname etc - if (!url.match(/^[a-zA-Z0-9-]+:/) && !url.match(/^(\/|\?)/)) { - return {save: url, display: url}; - } - - let parsedUrl: URL; - - try { - parsedUrl = new URL(url, baseUrl); - } catch (e) { - return {save: url, display: url}; - } - - if (!baseUrl) { - return {save: parsedUrl.toString(), display: parsedUrl.toString()}; - } - const parsedBaseUrl = new URL(baseUrl); - - let isRelativeToBasePath = parsedUrl.pathname && parsedUrl.pathname.indexOf(parsedBaseUrl.pathname) === 0; - - // if our path is only missing a trailing / mark it as relative - if (`${parsedUrl.pathname}/` === parsedBaseUrl.pathname) { - isRelativeToBasePath = true; - } - - const isOnSameHost = parsedUrl.host === parsedBaseUrl.host; - - // if relative to baseUrl, remove the base url before sending to action - if (isOnSameHost && isRelativeToBasePath) { - url = url.replace(/^[a-zA-Z0-9-]+:/, ''); - url = url.replace(/^\/\//, ''); - url = url.replace(parsedBaseUrl.host, ''); - url = url.replace(parsedBaseUrl.pathname, ''); - - if (!url.match(/^\//)) { - url = `/${url}`; - } - } - - if (!url.match(/\/$/) && !url.match(/[.#?]/)) { - url = `${url}/`; - } - - // we update with the relative URL but then transform it back to absolute - // for the input value. This avoids problems where the underlying relative - // value hasn't changed even though the input value has - return {save: url, display: displayFromBase(url, baseUrl)}; -}; - -const displayFromBase = (url: string, baseUrl: string) => { - // Ensure base url has a trailing slash - if (!baseUrl.endsWith('/')) { - baseUrl += '/'; - } - - // Remove leading slash from url - if (url.startsWith('/')) { - url = url.substring(1); - } - - return new URL(url, baseUrl).toString(); -}; diff --git a/apps/shade/test/unit/utils/formatUrl.test.ts b/apps/shade/test/unit/utils/formatUrl.test.ts index 2f61dd649b6f..e3c958ede3b8 100644 --- a/apps/shade/test/unit/utils/formatUrl.test.ts +++ b/apps/shade/test/unit/utils/formatUrl.test.ts @@ -1,5 +1,5 @@ import * as assert from 'assert/strict'; -import {formatUrl} from '../../../src/utils/formatUrl'; +import {formatUrl} from '@/lib/utils'; describe('formatUrl', function () { it('displays empty string if the input is empty and nullable is true', function () { diff --git a/apps/shade/tsconfig.json b/apps/shade/tsconfig.json index 8c59ec214bc3..5ed4b63be5d5 100644 --- a/apps/shade/tsconfig.json +++ b/apps/shade/tsconfig.json @@ -4,7 +4,7 @@ "lib": ["DOM", "DOM.Iterable", "ESNext"], "module": "ESNext", "skipLibCheck": true, - "types": ["vite/client"], + "types": ["vite/client", "mocha"], /* Bundler mode */ "moduleResolution": "bundler", @@ -23,6 +23,6 @@ "@/*": ["./src/*"] } }, - "include": ["src"], + "include": ["src", "test"], "references": [{ "path": "./tsconfig.node.json" }] } From c0ccdbe280081560d2d095ea570492252447e1bf Mon Sep 17 00:00:00 2001 From: Sam Lord Date: Thu, 23 Jan 2025 12:02:53 +0000 Subject: [PATCH 22/27] Portal: Added HCaptcha element to signup/signin pages ref BAE-371 Added the HCaptcha react component & related utils to enable it / disable it based on the Captcha labs flag. At the moment this does not include the same functionality on forms using the data-attributes. --- apps/portal/package.json | 3 ++ .../portal/src/components/pages/SigninPage.js | 34 ++++++++++++++++--- .../portal/src/components/pages/SignupPage.js | 32 ++++++++++++++--- .../src/components/pages/SignupPage.test.js | 19 ++++++++++- apps/portal/src/utils/api.js | 6 ++-- apps/portal/src/utils/fixtures-generator.js | 8 +++-- apps/portal/src/utils/helpers.js | 8 +++++ yarn.lock | 20 +++++++++++ 8 files changed, 116 insertions(+), 14 deletions(-) diff --git a/apps/portal/package.json b/apps/portal/package.json index 598b7dd3c476..26ae06d50d96 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -97,5 +97,8 @@ "vite-plugin-css-injected-by-js": "3.3.0", "vite-plugin-svgr": "3.3.0", "vitest": "0.34.3" + }, + "dependencies": { + "@hcaptcha/react-hcaptcha": "1.11.1" } } diff --git a/apps/portal/src/components/pages/SigninPage.js b/apps/portal/src/components/pages/SigninPage.js index 9adb3a08ffa1..0647af916e6f 100644 --- a/apps/portal/src/components/pages/SigninPage.js +++ b/apps/portal/src/components/pages/SigninPage.js @@ -5,8 +5,9 @@ import CloseButton from '../common/CloseButton'; import AppContext from '../../AppContext'; import InputForm from '../common/InputForm'; import {ValidateInputForm} from '../../utils/form'; -import {hasAvailablePrices, isSigninAllowed, isSignupAllowed} from '../../utils/helpers'; +import {hasAvailablePrices, isSigninAllowed, isSignupAllowed, hasCaptchaEnabled, getCaptchaSitekey} from '../../utils/helpers'; import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg'; +import HCaptcha from '@hcaptcha/react-hcaptcha'; export default class SigninPage extends React.Component { static contextType = AppContext; @@ -14,8 +15,12 @@ export default class SigninPage extends React.Component { constructor(props) { super(props); this.state = { - email: '' + email: '', + captchaLoaded: false, + token: undefined }; + + this.captchaRef = React.createRef(); } componentDidMount() { @@ -29,16 +34,27 @@ export default class SigninPage extends React.Component { handleSignin(e) { e.preventDefault(); + + const {site} = this.context; + if (hasCaptchaEnabled({site})) { + // hCaptcha's callback will call doSignin + return this.captchaRef.current.execute(); + } else { + this.doSignin(); + } + } + + doSignin() { this.setState((state) => { return { errors: ValidateInputForm({fields: this.getInputFields({state}), t: this.context.t}) }; }, async () => { - const {email, phonenumber, errors} = this.state; + const {email, phonenumber, errors, token} = this.state; const {redirect} = this.context.pageData ?? {}; const hasFormErrors = (errors && Object.values(errors).filter(d => !!d).length > 0); if (!hasFormErrors) { - this.context.onAction('signin', {email, phonenumber, redirect}); + this.context.onAction('signin', {email, phonenumber, redirect, token}); } }); } @@ -156,6 +172,16 @@ export default class SigninPage extends React.Component { onChange={(e, field) => this.handleInputChange(e, field)} onKeyDown={(e, field) => this.onKeyDown(e, field)} /> + {(hasCaptchaEnabled({site}) && + this.setState({captchaLoaded: true})} + onVerify={token => this.setState({token: token}, this.doSignin)} + ref={this.captchaRef} + id="hcaptcha-signin" + /> + )}
    {this.renderSubmitButton()} diff --git a/apps/portal/src/components/pages/SignupPage.js b/apps/portal/src/components/pages/SignupPage.js index f46e944b1ac1..7e7b40eea20f 100644 --- a/apps/portal/src/components/pages/SignupPage.js +++ b/apps/portal/src/components/pages/SignupPage.js @@ -7,9 +7,10 @@ import NewsletterSelectionPage from './NewsletterSelectionPage'; import ProductsSection from '../common/ProductsSection'; import InputForm from '../common/InputForm'; import {ValidateInputForm} from '../../utils/form'; -import {getSiteProducts, getSitePrices, hasAvailablePrices, hasOnlyFreePlan, isInviteOnly, isFreeSignupAllowed, isPaidMembersOnly, freeHasBenefitsOrDescription, hasMultipleNewsletters, hasFreeTrialTier, isSignupAllowed, isSigninAllowed} from '../../utils/helpers'; +import {getSiteProducts, getSitePrices, hasAvailablePrices, hasOnlyFreePlan, isInviteOnly, isFreeSignupAllowed, isPaidMembersOnly, freeHasBenefitsOrDescription, hasMultipleNewsletters, hasFreeTrialTier, isSignupAllowed, isSigninAllowed, hasCaptchaEnabled, getCaptchaSitekey} from '../../utils/helpers'; import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg'; import {interceptAnchorClicks} from '../../utils/links'; +import HCaptcha from '@hcaptcha/react-hcaptcha'; export const SignupPageStyles = ` .gh-portal-back-sitetitle { @@ -356,6 +357,7 @@ class SignupPage extends React.Component { }; this.termsRef = React.createRef(); + this.captchaRef = React.createRef(); } componentDidMount() { @@ -400,6 +402,16 @@ class SignupPage extends React.Component { }; } + doSignupWithChecks() { + const {site} = this.context; + if (hasCaptchaEnabled({site})) { + // hCaptcha's callback will call doSignup + return this.captchaRef.current.execute(); + } else { + this.doSignup(); + } + } + doSignup() { this.setState((state) => { return { @@ -407,7 +419,7 @@ class SignupPage extends React.Component { }; }, () => { const {site, onAction} = this.context; - const {name, email, plan, phonenumber, errors} = this.state; + const {name, email, plan, phonenumber, token, errors} = this.state; const hasFormErrors = (errors && Object.values(errors).filter(d => !!d).length > 0); // Only scroll checkbox into view if it's the only error @@ -423,14 +435,14 @@ class SignupPage extends React.Component { if (hasMultipleNewsletters({site})) { this.setState({ showNewsletterSelection: true, - pageData: {name, email, plan, phonenumber}, + pageData: {name, email, plan, phonenumber, token}, errors: {} }); } else { this.setState({ errors: {} }); - onAction('signup', {name, email, phonenumber, plan}); + onAction('signup', {name, email, phonenumber, plan, token}); } } }); @@ -444,7 +456,7 @@ class SignupPage extends React.Component { handleChooseSignup(e, plan) { e.preventDefault(); this.setState({plan}, () => { - this.doSignup(); + this.doSignupWithChecks(); }); } @@ -732,6 +744,16 @@ class SignupPage extends React.Component { onChange={(e, field) => this.handleInputChange(e, field)} onKeyDown={e => this.onKeyDown(e)} /> + {(hasCaptchaEnabled({site}) && + this.setState({captchaLoaded: true})} + onVerify={token => this.setState({token: token}, this.doSignup)} + ref={this.captchaRef} + id="hcaptcha-signup" + /> + )}
    {(hasOnlyFree ? diff --git a/apps/portal/src/components/pages/SignupPage.test.js b/apps/portal/src/components/pages/SignupPage.test.js index 852934211cec..5e69b1564f92 100644 --- a/apps/portal/src/components/pages/SignupPage.test.js +++ b/apps/portal/src/components/pages/SignupPage.test.js @@ -1,6 +1,6 @@ import SignupPage from './SignupPage'; import {getFreeProduct, getProductData, getSiteData} from '../../utils/fixtures-generator'; -import {render, fireEvent, getByTestId, queryByTestId} from '../../utils/test-utils'; +import {render, fireEvent, getByTestId, queryByTestId, queryByAttribute} from '../../utils/test-utils'; const setup = (overrides) => { const {mockOnActionFn, ...utils} = render( @@ -212,4 +212,21 @@ describe('SignupPage', () => { expect(signinLink).toBeInTheDocument(); }); }); + + // Cannot test using hCaptcha component, as it cannot run in a test environment + describe('when captcha is enabled', () => { + test('renders', () => { + setup({ + site: getSiteData({ + captchaEnabled: true, + captchaSiteKey: '20000000-ffff-ffff-ffff-000000000002' + }) + }); + + const getById = queryByAttribute.bind(null, 'id'); + + const hcaptchaElement = getById(document.body, 'hcaptcha-signup'); + expect(hcaptchaElement).toBeInTheDocument(); + }); + }); }); diff --git a/apps/portal/src/utils/api.js b/apps/portal/src/utils/api.js index 55bd0a8536ad..41abc9263e3d 100644 --- a/apps/portal/src/utils/api.js +++ b/apps/portal/src/utils/api.js @@ -262,7 +262,7 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) { } }, - async sendMagicLink({email, emailType, labels, name, oldEmail, newsletters, redirect, integrityToken, phonenumber, customUrlHistory, autoRedirect = true}) { + async sendMagicLink({email, emailType, labels, name, oldEmail, newsletters, redirect, integrityToken, phonenumber, customUrlHistory, token, autoRedirect = true}) { const url = endpointFor({type: 'members', resource: 'send-magic-link'}); const body = { name, @@ -274,7 +274,9 @@ function setupGhostApi({siteUrl = window.location.origin, apiUrl, apiKey}) { requestSrc: 'portal', redirect, integrityToken, - honeypot: phonenumber, // we don't actually use a phone #, this is from a hidden field to prevent bot activity + // we don't actually use a phone #, this is from a hidden field to prevent bot activity + honeypot: phonenumber, + token, autoRedirect }; const urlHistory = customUrlHistory ?? getUrlHistory(); diff --git a/apps/portal/src/utils/fixtures-generator.js b/apps/portal/src/utils/fixtures-generator.js index 72f8075e80a9..55f1455018f9 100644 --- a/apps/portal/src/utils/fixtures-generator.js +++ b/apps/portal/src/utils/fixtures-generator.js @@ -43,7 +43,9 @@ export function getSiteData({ posts = getPostsData(), commentsEnabled, recommendations = [], - recommendationsEnabled + recommendationsEnabled, + captchaEnabled = false, + captchaSiteKey } = {}) { return { title, @@ -71,7 +73,9 @@ export function getSiteData({ recommendations, recommendations_enabled: !!recommendationsEnabled, editor_default_email_recipients, - posts + posts, + captcha_enabled: !!captchaEnabled, + captcha_sitekey: captchaSiteKey }; } diff --git a/apps/portal/src/utils/helpers.js b/apps/portal/src/utils/helpers.js index ce70d3b9a3d7..df0472b7aa3b 100644 --- a/apps/portal/src/utils/helpers.js +++ b/apps/portal/src/utils/helpers.js @@ -511,6 +511,14 @@ export function getSiteNewsletters({site}) { return newsletters; } +export function hasCaptchaEnabled({site}) { + return site?.captcha_enabled === true; +} + +export function getCaptchaSitekey({site}) { + return site?.captcha_sitekey || ''; +} + export function hasMultipleNewsletters({site}) { const { newsletters diff --git a/yarn.lock b/yarn.lock index 5ba170a3f824..1fcff823eb6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1875,6 +1875,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.17.9": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.20.7", "@babel/template@^7.22.15", "@babel/template@^7.24.7", "@babel/template@^7.3.3": version "7.24.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" @@ -3337,6 +3344,19 @@ resolved "https://registry.yarnpkg.com/@handlebars/parser/-/parser-2.0.0.tgz#5e8b7298f31ff8f7b260e6b7363c7e9ceed7d9c5" integrity sha512-EP9uEDZv/L5Qh9IWuMUGJRfwhXJ4h1dqKTT4/3+tY0eu7sPis7xh23j61SYUnNF4vqCQvvUXpDo9Bh/+q1zASA== +"@hcaptcha/loader@^1.2.1": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@hcaptcha/loader/-/loader-1.2.4.tgz#541714395a82e27ec0f0e8bd80ef1a0bea141cc3" + integrity sha512-3MNrIy/nWBfyVVvMPBKdKrX7BeadgiimW0AL/a/8TohNtJqxoySKgTJEXOQvYwlHemQpUzFrIsK74ody7JiMYw== + +"@hcaptcha/react-hcaptcha@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.11.1.tgz#cc0dd0b180b3458ca7b0f8cf1446f09afca2ec55" + integrity sha512-g6TwatNIzBtOR3RM4mxzvTUQGs5T9HMN+4fcNGHn7wUVThvmazThUs0vImI836bSkGpJS8n0rOYvv1UZ47q8Vw== + dependencies: + "@babel/runtime" "^7.17.9" + "@hcaptcha/loader" "^1.2.1" + "@headlessui/react@1.7.19": version "1.7.19" resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.19.tgz#91c78cf5fcb254f4a0ebe96936d48421caf75f40" From 19d9c3e3e2cb847bc2b20f9adff631e8b7bcc986 Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Thu, 23 Jan 2025 16:48:29 +0100 Subject: [PATCH 23/27] Post analytics router update (#22050) ref https://linear.app/ghost/issue/DES-1082/router-prototype This task is about testing, figuring out pros and cons of React Router compared to our current (custom) router, and what effort and risks are involved in migrating to it. --- apps/admin-x-framework/package.json | 3 +- apps/admin-x-framework/src/index.ts | 13 ++- .../src/providers/RouterProvider.tsx | 81 +++++++++++++++++++ apps/posts/package.json | 4 +- apps/posts/src/App.tsx | 7 +- apps/posts/src/components/Header.tsx | 71 +++++++++------- apps/posts/src/routes.ts | 20 +++-- .../views/post-analytics/PostAnalytics.tsx | 7 +- .../post-analytics/modals/ShareModal.tsx | 16 ++++ apps/posts/src/views/posts/Posts.tsx | 21 +++++ .../src/components/layout/error-page.tsx | 23 ++++++ apps/shade/src/components/ui/card.stories.tsx | 2 +- .../src/components/ui/dialog.stories.tsx | 10 ++- apps/shade/src/index.ts | 1 + .../admin/app/components/gh-nav-menu/main.hbs | 2 +- ghost/admin/app/router.js | 3 - 16 files changed, 229 insertions(+), 55 deletions(-) create mode 100644 apps/admin-x-framework/src/providers/RouterProvider.tsx create mode 100644 apps/posts/src/views/post-analytics/modals/ShareModal.tsx create mode 100644 apps/posts/src/views/posts/Posts.tsx create mode 100644 apps/shade/src/components/layout/error-page.tsx diff --git a/apps/admin-x-framework/package.json b/apps/admin-x-framework/package.json index 2c22e5288dc0..cf474f2eb506 100644 --- a/apps/admin-x-framework/package.json +++ b/apps/admin-x-framework/package.json @@ -91,6 +91,7 @@ "@vitejs/plugin-react": "4.2.1", "react": "18.3.1", "react-dom": "18.3.1", + "react-router": "^7.1.3", "vite": "4.5.3", "vite-plugin-css-injected-by-js": "^3.3.0", "vite-plugin-svgr": "3.3.0", @@ -116,4 +117,4 @@ } } } -} \ No newline at end of file +} diff --git a/apps/admin-x-framework/src/index.ts b/apps/admin-x-framework/src/index.ts index f87665942141..06d0cbf98d2e 100644 --- a/apps/admin-x-framework/src/index.ts +++ b/apps/admin-x-framework/src/index.ts @@ -1,6 +1,13 @@ -export {FrameworkProvider, useFramework} from './providers/FrameworkProvider'; +// Framework export type {FrameworkContextType, FrameworkProviderProps, TopLevelFrameworkProps} from './providers/FrameworkProvider'; +export {FrameworkProvider, useFramework} from './providers/FrameworkProvider'; -export {useQueryClient} from '@tanstack/react-query'; -export type {InfiniteData} from '@tanstack/react-query'; +// Routing +export type {RouteObject} from 'react-router'; +export type {RouterProviderProps} from './providers/RouterProvider'; +export {RouterProvider, useNavigate} from './providers/RouterProvider'; +export {useLocation, useParams, useSearchParams, Outlet} from 'react-router'; +// Data fetching +export type {InfiniteData} from '@tanstack/react-query'; +export {useQueryClient} from '@tanstack/react-query'; diff --git a/apps/admin-x-framework/src/providers/RouterProvider.tsx b/apps/admin-x-framework/src/providers/RouterProvider.tsx new file mode 100644 index 000000000000..b039d3a29602 --- /dev/null +++ b/apps/admin-x-framework/src/providers/RouterProvider.tsx @@ -0,0 +1,81 @@ +import {ErrorPage} from '@tryghost/shade'; +import React, {useCallback, useMemo} from 'react'; +import {createHashRouter, RouteObject, RouterProvider as ReactRouterProvider, NavigateOptions as ReactRouterNavigateOptions, useNavigate as useReactRouterNavigate} from 'react-router'; +import {useFramework} from './FrameworkProvider'; + +/** + * READ THIS BEFORE USING THIS PROVIDER + * + * This is an experimental provider that tests using React Router to provide + * a router context to React apps in Ghost. + * + * It is not ready for production yet. For apps in production, use the custom + * RoutingProvider. + */ + +/** + * Wrap React Router in a custom provider to provide a standard, simplified + * interface for all Ghost apps for routing. It also sanitizes the routes and + * adds a default error element. + */ +export interface RouterProviderProps { + routes: RouteObject[]; + prefix?: string; + + // Custom routing props + errorElement?: React.ReactNode; +} + +export function RouterProvider({ + routes, + prefix, + errorElement +}: RouterProviderProps) { + // Memoize the router to avoid re-creating it on every render + const router = useMemo(() => { + // Ensure prefix has a leading slash and no double+ or trailing slashes + const normalizedPrefix = `/${prefix?.replace(/\/+/g, '/').replace(/^\/|\/$/g, '')}`; + + // Add default error element if not provided + const finalRoutes = routes.map(route => ({ + ...route, + errorElement: route.errorElement || errorElement || + })); + + return createHashRouter(finalRoutes, { + basename: normalizedPrefix + }); + }, [routes, prefix, errorElement]); + + return ( + + ); +} + +/** + * Override the default navigate function to add the crossApp option. This is + * used to determine if the navigate should be handled by the custom router, ie. + * if we need to navigate outside of the current app in Ghost. + */ +interface NavigateOptions extends ReactRouterNavigateOptions { + crossApp?: boolean; +} + +export function useNavigate() { + const navigate = useReactRouterNavigate(); + const {externalNavigate} = useFramework(); + + return useCallback(( + to: string, + options?: NavigateOptions + ) => { + if (options?.crossApp) { + externalNavigate({route: to, isExternal: true}); + return; + } + + navigate(to, options); + }, [navigate, externalNavigate]); +} diff --git a/apps/posts/package.json b/apps/posts/package.json index 0c51c321116d..8997a01b964d 100644 --- a/apps/posts/package.json +++ b/apps/posts/package.json @@ -52,7 +52,5 @@ } } }, - "dependencies": { - "react-router": "^7.1.3" - } + "dependencies": {} } diff --git a/apps/posts/src/App.tsx b/apps/posts/src/App.tsx index 94899476e4e5..72c6a102888c 100644 --- a/apps/posts/src/App.tsx +++ b/apps/posts/src/App.tsx @@ -1,7 +1,6 @@ -import {FrameworkProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework'; -import {RouterProvider} from 'react-router'; +import {APP_ROUTE_PREFIX, routes} from './routes'; +import {FrameworkProvider, RouterProvider, TopLevelFrameworkProps} from '@tryghost/admin-x-framework'; import {ShadeApp, ShadeAppProps, SidebarProvider} from '@tryghost/shade'; -import {router} from './routes'; interface AppProps { framework: TopLevelFrameworkProps; @@ -13,7 +12,7 @@ const App: React.FC = ({framework, designSystem}) => { - + diff --git a/apps/posts/src/components/Header.tsx b/apps/posts/src/components/Header.tsx index f8b2d50c0b12..6e1bf1179ec4 100644 --- a/apps/posts/src/components/Header.tsx +++ b/apps/posts/src/components/Header.tsx @@ -1,15 +1,30 @@ -import {Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, H1, LucideIcon} from '@tryghost/shade'; +import ShareModal from '../views/post-analytics/modals/ShareModal'; +import {Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, H1, LucideIcon} from '@tryghost/shade'; +import {useLocation, useNavigate, useParams} from '@tryghost/admin-x-framework'; interface headerProps {}; const Header: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const {postId} = useParams(); + + // Handling the share dialog via navigation + const isShareDialogOpen = location.pathname === `/analytics/${postId}/share`; + const openShareDialog = () => { + navigate(`/analytics/${postId}/share`); + }; + const closeShareDialog = () => { + navigate(`/analytics/${postId}`); + }; + return (
    - + navigate('/')}> Posts @@ -22,36 +37,38 @@ const Header: React.FC = () => {
    + + + + - - - - + + + + + + + Edit post + ⇧⌘E + + + View in browser + ⇧⌘O + + + + + Delete + + + + + - Share - - This is a dialog, lets see how it works. - + Are you sure you want to delete this post? - - - - - - - Edit post - ⇧⌘E - - - View in browser - ⇧⌘O - - - Delete - -

    The Evolution of Basketball: From Pastime to Professional and One of the Most Popular Sports

    diff --git a/apps/posts/src/routes.ts b/apps/posts/src/routes.ts index 8e1f20afd829..11d75ea35323 100644 --- a/apps/posts/src/routes.ts +++ b/apps/posts/src/routes.ts @@ -1,14 +1,18 @@ import Newsletter from './views/post-analytics/components/Newsletter'; import Overview from './views/post-analytics/components/Overview'; import PostAnalytics from './views/post-analytics/PostAnalytics'; -import {createHashRouter} from 'react-router'; +import Posts from './views/posts/Posts'; +import {RouteObject} from '@tryghost/admin-x-framework'; -export const BASE_PATH = '/posts-x'; -export const ANALYTICS = `${BASE_PATH}/analytics`; +export const APP_ROUTE_PREFIX = '/posts-x'; -const postAnalyticsRoutes = [ +export const routes: RouteObject[] = [ { - path: `${BASE_PATH}/analytics/:postId`, + path: '', + Component: Posts + }, + { + path: 'analytics/:postId', Component: PostAnalytics, children: [ { @@ -22,9 +26,11 @@ const postAnalyticsRoutes = [ { path: 'newsletter', Component: Newsletter + }, + { + path: 'share', + Component: Overview } ] } ]; - -export const router = createHashRouter(postAnalyticsRoutes); diff --git a/apps/posts/src/views/post-analytics/PostAnalytics.tsx b/apps/posts/src/views/post-analytics/PostAnalytics.tsx index ebb78a58f63f..39e6e0dc6c50 100644 --- a/apps/posts/src/views/post-analytics/PostAnalytics.tsx +++ b/apps/posts/src/views/post-analytics/PostAnalytics.tsx @@ -1,6 +1,5 @@ import Header from '../../components/Header'; -import {ANALYTICS} from '../../routes'; -import {Outlet, useLocation, useNavigate, useParams} from 'react-router'; +import {Outlet, useLocation, useNavigate, useParams} from '@tryghost/admin-x-framework'; import {Page, Tabs, TabsList, TabsTrigger} from '@tryghost/shade'; interface postAnalyticsProps {}; @@ -17,9 +16,9 @@ const PostAnalytics: React.FC = () => { const handleTabChange = (value: string) => { if (value === 'overview') { - navigate(`${ANALYTICS}/${postId}`); + navigate(`analytics/${postId}`); } else { - navigate(`${ANALYTICS}/${postId}/${value}`); + navigate(`analytics/${postId}/${value}`); } }; diff --git a/apps/posts/src/views/post-analytics/modals/ShareModal.tsx b/apps/posts/src/views/post-analytics/modals/ShareModal.tsx new file mode 100644 index 000000000000..ba0af18e66a6 --- /dev/null +++ b/apps/posts/src/views/post-analytics/modals/ShareModal.tsx @@ -0,0 +1,16 @@ +import {DialogContent, DialogDescription, DialogHeader, DialogTitle} from '@tryghost/shade'; + +const ShareModal = () => { + return ( + + + Share + + This is a dialog opened with router and with a custom width. + + + + ); +}; + +export default ShareModal; \ No newline at end of file diff --git a/apps/posts/src/views/posts/Posts.tsx b/apps/posts/src/views/posts/Posts.tsx new file mode 100644 index 000000000000..86f0e8d52b4c --- /dev/null +++ b/apps/posts/src/views/posts/Posts.tsx @@ -0,0 +1,21 @@ +import {Button, H1, Page} from '@tryghost/shade'; +import {useNavigate} from '@tryghost/admin-x-framework'; + +interface postAnalyticsProps {}; + +const Posts: React.FC = () => { + const navigate = useNavigate(); + + return ( + +

    Posts

    +
    + + + +
    +
    + ); +}; + +export default Posts; diff --git a/apps/shade/src/components/layout/error-page.tsx b/apps/shade/src/components/layout/error-page.tsx new file mode 100644 index 000000000000..6625e589d87c --- /dev/null +++ b/apps/shade/src/components/layout/error-page.tsx @@ -0,0 +1,23 @@ +import {cn} from '@/lib/utils'; +import * as React from 'react'; + +export interface ErrorPageProps + extends React.HTMLAttributes {} + +const ErrorPage = React.forwardRef( + ({className, ...props}, ref) => { + return ( +
    +

    Error

    +
    + ); + } +); + +ErrorPage.displayName = 'ErrorPage'; + +export {ErrorPage}; diff --git a/apps/shade/src/components/ui/card.stories.tsx b/apps/shade/src/components/ui/card.stories.tsx index e81ca6418ca9..469439e9eba5 100644 --- a/apps/shade/src/components/ui/card.stories.tsx +++ b/apps/shade/src/components/ui/card.stories.tsx @@ -24,7 +24,7 @@ export const Default: Story = { Card contents , - + diff --git a/apps/shade/src/components/ui/dialog.stories.tsx b/apps/shade/src/components/ui/dialog.stories.tsx index 4a4727b8fce1..eaaaacc6b3d9 100644 --- a/apps/shade/src/components/ui/dialog.stories.tsx +++ b/apps/shade/src/components/ui/dialog.stories.tsx @@ -1,5 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react'; -import {Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle} from './dialog'; +import {Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription} from './dialog'; import {Button} from './button'; const meta = { @@ -26,7 +26,15 @@ export const Default: Story = { Are you absolutely sure? + + This action cannot be undone. Are you sure you want to permanently + delete this file from our servers? + + + + + ) diff --git a/apps/shade/src/index.ts b/apps/shade/src/index.ts index f45cd4a989f9..41f51c948030 100644 --- a/apps/shade/src/index.ts +++ b/apps/shade/src/index.ts @@ -19,6 +19,7 @@ export * from './components/ui/tooltip'; // Layout components export * from './components/layout/page'; +export * from './components/layout/error-page'; export * from './components/layout/heading'; // Third party components diff --git a/ghost/admin/app/components/gh-nav-menu/main.hbs b/ghost/admin/app/components/gh-nav-menu/main.hbs index 1a5392416fa8..b1d75ec98368 100644 --- a/ghost/admin/app/components/gh-nav-menu/main.hbs +++ b/ghost/admin/app/components/gh-nav-menu/main.hbs @@ -164,7 +164,7 @@ {{/if}} {{#if (feature "postsX")}}
  • - {{svg-jar "chart"}}Post analytics + {{svg-jar "chart"}}Post analytics
  • {{/if}} diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js index 5c0a1352a1a6..5fa7d260a088 100644 --- a/ghost/admin/app/router.js +++ b/ghost/admin/app/router.js @@ -54,9 +54,6 @@ Router.map(function () { this.route('posts-x', function () { this.route('posts-x', {path: '/*sub'}); - this.route('analytics', function () { - this.route('123'); - }); }); this.route('settings-x', {path: '/settings'}, function () { From 8c2e62dc23043e03bdafa2ab0821dbccdfbabc18 Mon Sep 17 00:00:00 2001 From: Djordje Vlaisavljevic Date: Thu, 23 Jan 2025 17:35:05 +0000 Subject: [PATCH 24/27] Improved ActivityPub design (#22051) ref https://linear.app/ghost/issue/AP-677/standardize-border-radius-used-in-avatars, https://linear.app/ghost/issue/AP-680/standardize-font-sizes-colors-and-weights, https://linear.app/ghost/issue/AP-676/improve-the-sidebar-widget - Ensured consistent use of border-radius in avatars - Removed onClick for large avatars, since we only use them when you're already viewing someone's profile - Updated font colors, weights and sizes for consistency - Updated design of the sidebar widget in (simpler design, less lines, tighter spacing= - "Explore" button looks more like what we use in settings and dashboard --- apps/admin-x-activitypub/package.json | 2 +- .../src/components/Inbox.tsx | 10 +++---- .../src/components/Search.tsx | 4 +-- .../src/components/feed/ArticleModal.tsx | 2 +- .../src/components/feed/FeedItem.tsx | 9 +++---- .../src/components/global/APAvatar.tsx | 26 ++++++++++--------- apps/admin-x-activitypub/src/styles/index.css | 5 ++-- 7 files changed, 28 insertions(+), 30 deletions(-) diff --git a/apps/admin-x-activitypub/package.json b/apps/admin-x-activitypub/package.json index 1d51e5499d60..26a555f3578a 100644 --- a/apps/admin-x-activitypub/package.json +++ b/apps/admin-x-activitypub/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/admin-x-activitypub", - "version": "0.3.53", + "version": "0.3.54", "license": "MIT", "repository": { "type": "git", diff --git a/apps/admin-x-activitypub/src/components/Inbox.tsx b/apps/admin-x-activitypub/src/components/Inbox.tsx index 4fb7fc753725..9f4c3e5b5466 100644 --- a/apps/admin-x-activitypub/src/components/Inbox.tsx +++ b/apps/admin-x-activitypub/src/components/Inbox.tsx @@ -137,9 +137,9 @@ const Inbox: React.FC = ({layout}) => {
    -

    This is your {layout === 'inbox' ? 'inbox' : 'feed'}

    -

    You'll find {layout === 'inbox' ? 'long-form content' : 'short posts and updates'} from the accounts you follow here.

    -

    You might also like

    +

    This is your {layout === 'inbox' ? 'inbox' : 'feed'}

    +

    You'll find {layout === 'inbox' ? 'long-form content' : 'short posts and updates'} from the accounts you follow here.

    +

    You might also like

    {isLoadingSuggested ? ( ) : ( @@ -154,7 +154,7 @@ const Inbox: React.FC = ({layout}) => { >
    - {getName(actor)} + {getName(actor)} {getUsername(actor)}
    @@ -165,7 +165,7 @@ const Inbox: React.FC = ({layout}) => { })} )} -
    diff --git a/apps/admin-x-activitypub/src/components/Search.tsx b/apps/admin-x-activitypub/src/components/Search.tsx index 2c63000fab9d..7062408724dd 100644 --- a/apps/admin-x-activitypub/src/components/Search.tsx +++ b/apps/admin-x-activitypub/src/components/Search.tsx @@ -59,7 +59,7 @@ const AccountSearchResultItem: React.FC = ({accoun }}/>
    - {account.name} {account.handle} + {account.name} {account.handle}
    {new Intl.NumberFormat().format(account.followerCount)} followers
    @@ -124,7 +124,7 @@ const SuggestedProfile: React.FC = ({profile, update}) =>
    - {profile.actor.name} {profile.handle} + {profile.actor.name} {profile.handle}
    {new Intl.NumberFormat().format(profile.followerCount)} followers
    diff --git a/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx b/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx index d1a2ee92cc61..67461928c1a9 100644 --- a/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx +++ b/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx @@ -718,7 +718,7 @@ const ArticleModal: React.FC = ({
    - {actor.name} + {actor.name}
    {getUsername(actor)} diff --git a/apps/admin-x-activitypub/src/components/feed/FeedItem.tsx b/apps/admin-x-activitypub/src/components/feed/FeedItem.tsx index 068c5b888e24..410d8960f670 100644 --- a/apps/admin-x-activitypub/src/components/feed/FeedItem.tsx +++ b/apps/admin-x-activitypub/src/components/feed/FeedItem.tsx @@ -7,7 +7,6 @@ import APAvatar from '../global/APAvatar'; import FeedItemStats from './FeedItemStats'; import clsx from 'clsx'; import getReadingTime from '../../utils/get-reading-time'; -import getRelativeTimestamp from '../../utils/get-relative-timestamp'; import getUsername from '../../utils/get-username'; import stripHtml from '../../utils/strip-html'; import {handleProfileClick} from '../../utils/handle-profile-click'; @@ -161,8 +160,6 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment 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 date = new Date(object?.published ?? new Date()); - const [, setIsCopied] = useState(false); const contentRef = useRef(null); @@ -324,7 +321,7 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment
    - {author.name} + {author.name}
    {renderTimestamp(object)}
    @@ -375,7 +372,7 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment
    - {author.name} + {author.name}
    {renderTimestamp(object)}
    @@ -432,7 +429,7 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment onClick={e => handleProfileClick(actor, e)} >{author.name} - {getRelativeTimestamp(date)} + {renderTimestamp(object)}
    diff --git a/apps/admin-x-activitypub/src/components/global/APAvatar.tsx b/apps/admin-x-activitypub/src/components/global/APAvatar.tsx index 62190fe86faa..d9ed572e3115 100644 --- a/apps/admin-x-activitypub/src/components/global/APAvatar.tsx +++ b/apps/admin-x-activitypub/src/components/global/APAvatar.tsx @@ -21,8 +21,8 @@ interface APAvatarProps { const APAvatar: React.FC = ({author, size}) => { let iconSize = 18; - let containerClass = `shrink-0 items-center justify-center relative cursor-pointer z-10 flex ${size === 'lg' ? '' : 'hover:opacity-80'}`; - let imageClass = 'z-10 rounded-md w-10 h-10 object-cover'; + let containerClass = `shrink-0 items-center justify-center overflow-hidden relative z-10 flex ${size === 'lg' ? '' : 'hover:opacity-80 cursor-pointer'}`; + let imageClass = 'z-10 object-cover'; const [iconUrl, setIconUrl] = useState(author?.icon?.url); useEffect(() => { @@ -36,28 +36,30 @@ const APAvatar: React.FC = ({author, size}) => { switch (size) { case '2xs': iconSize = 10; - containerClass = clsx('h-4 w-4 rounded-md ', containerClass); - imageClass = 'z-10 rounded-md w-4 h-4 object-cover'; + containerClass = clsx('h-4 w-4 rounded-md', containerClass); + imageClass = clsx('h-4 w-4', imageClass); break; case 'xs': iconSize = 12; - containerClass = clsx('h-6 w-6 rounded-md ', containerClass); - imageClass = 'z-10 rounded-md w-6 h-6 object-cover'; + containerClass = clsx('h-6 w-6 rounded-lg', containerClass); + imageClass = clsx('h-6 w-6', imageClass); break; case 'notification': iconSize = 12; - containerClass = clsx('h-9 w-9 rounded-md', containerClass); - imageClass = 'z-10 rounded-xl w-9 h-9 object-cover'; + containerClass = clsx('h-9 w-9 rounded-lg', containerClass); + imageClass = clsx('h-9 w-9', imageClass); break; case 'sm': - containerClass = clsx('h-10 w-10 rounded-md', containerClass); + containerClass = clsx('h-10 w-10 rounded-xl', containerClass); + imageClass = clsx('h-10 w-10', imageClass); break; case 'lg': containerClass = clsx('h-22 w-22 rounded-xl', containerClass); - imageClass = 'z-10 rounded-xl w-22 h-22 object-cover'; + imageClass = clsx('h-22 w-22', imageClass); break; default: - containerClass = clsx('h-10 w-10 rounded-md', containerClass); + containerClass = clsx('h-10 w-10 rounded-lg', containerClass); + imageClass = clsx('h-10 w-10', imageClass); break; } @@ -79,7 +81,7 @@ const APAvatar: React.FC = ({author, size}) => {
    * + * { - margin-top: 1.6rem !important; + margin-top: 1.65rem !important; } .ap-note-content > h1 + *, From 9589a9168401cbd897b6b2f65cc611a96f25c909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20der=20Winden?= Date: Fri, 24 Jan 2025 14:11:07 +0100 Subject: [PATCH 25/27] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20tags=20and=20autho?= =?UTF-8?q?rs=20not=20fitting=20in=20the=20input=20field=20(#22052)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Input fields for tags and authors in the post sidebar were hard to use; they became scrollable if you added more than one line of either. This fix addresses that; the input field now grows in size to accommodate for the number of tags or authors you enter. fixes https://linear.app/ghost/issue/DES-1087/overflow-on-boxes-in-post-settings-too-small, https://linear.app/ghost/issue/DES-1084/tag-field-in-post-settings-menu-is-difficult-to-work-now-with-when --- .../app/styles/components/power-select.css | 3 +- ghost/admin/app/styles/patterns/forms.css | 32 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/ghost/admin/app/styles/components/power-select.css b/ghost/admin/app/styles/components/power-select.css index cc1ed01aa7c2..5cf220442eae 100644 --- a/ghost/admin/app/styles/components/power-select.css +++ b/ghost/admin/app/styles/components/power-select.css @@ -309,7 +309,8 @@ } .ember-power-select-status-icon { - top: -4px; + position: absolute; + top: 13px; right: 13px; border: solid var(--midlightgrey); border-width: 0 1px 1px 0; diff --git a/ghost/admin/app/styles/patterns/forms.css b/ghost/admin/app/styles/patterns/forms.css index 922d4d4077f7..d1d3a81b99cd 100644 --- a/ghost/admin/app/styles/patterns/forms.css +++ b/ghost/admin/app/styles/patterns/forms.css @@ -282,7 +282,8 @@ textarea { .gh-input-x { width: 100%; - height: 38px; + min-height: 38px; + height: auto; padding: 4px 12px; font-size: 1.4rem; color: var(--black); @@ -305,7 +306,34 @@ textarea.gh-input-x { } .ember-power-select-multiple-trigger { - padding-left: 4px; + padding: 4px; + min-height: 38px; + display: grid; + grid-template-columns: 1fr 24px; + position: relative; +} + +.ember-power-select-multiple-options { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px; + margin: 0; + padding: 0; +} + +.ember-power-select-trigger-multiple-input { + margin: 2px; + padding: 0; + border: 0; + background: none; + min-width: 60px; +} + +.ember-basic-dropdown-trigger[aria-expanded="true"] .ember-power-select-status-icon, +.ember-power-select-status-icon { + margin: 0 auto; + transform: none; } From d2c868db00cb62825302e059bfe5bcaf953d25a5 Mon Sep 17 00:00:00 2001 From: Ghost CI <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Jan 2025 15:07:51 +0000 Subject: [PATCH 26/27] v5.108.0 --- ghost/admin/package.json | 2 +- ghost/core/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 2cd6914487a7..375e2185ce06 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -1,6 +1,6 @@ { "name": "ghost-admin", - "version": "5.107.2", + "version": "5.108.0", "description": "Ember.js admin client for Ghost", "author": "Ghost Foundation", "homepage": "http://ghost.org", diff --git a/ghost/core/package.json b/ghost/core/package.json index 8190ca2df046..15b9199f4adc 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -1,6 +1,6 @@ { "name": "ghost", - "version": "5.107.2", + "version": "5.108.0", "description": "The professional publishing platform", "author": "Ghost Foundation", "homepage": "https://ghost.org", From 3e2658baa0fe76ec0ee68b8cca439427adf4e09a Mon Sep 17 00:00:00 2001 From: Ghost CI <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 26 Jan 2025 17:53:15 +0000 Subject: [PATCH 27/27] v5.108.1 --- ghost/admin/package.json | 2 +- ghost/core/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 375e2185ce06..1e21da93267d 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -1,6 +1,6 @@ { "name": "ghost-admin", - "version": "5.108.0", + "version": "5.108.1", "description": "Ember.js admin client for Ghost", "author": "Ghost Foundation", "homepage": "http://ghost.org", diff --git a/ghost/core/package.json b/ghost/core/package.json index 15b9199f4adc..aac4af6a3b25 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -1,6 +1,6 @@ { "name": "ghost", - "version": "5.108.0", + "version": "5.108.1", "description": "The professional publishing platform", "author": "Ghost Foundation", "homepage": "https://ghost.org",