Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ import {useGlobalData} from '../../../providers/global-data-provider';
import {useRouting} from '@tryghost/admin-x-framework/routing';

const EmbedSignupFormModal = NiceModal.create(() => {
let i18nEnabled = false;

const [selectedColor, setSelectedColor] = useState<string>('#08090c');
const [selectedLabels, setSelectedLabels] = useState<SelectedLabelTypes[]>([]);
const [selectedLayout, setSelectedLayout] = useState<string>('all-in-one');
Expand All @@ -23,13 +21,9 @@ const EmbedSignupFormModal = NiceModal.create(() => {
const {updateRoute} = useRouting();
const {config} = useGlobalData();
const {localSettings, siteData} = useSettingGroup();
const [accentColor, title, description, locale, labs, icon] = getSettingValues<string>(localSettings, ['accent_color', 'title', 'description', 'locale', 'labs', 'icon']);
const [accentColor, title, description, locale, icon] = getSettingValues<string>(localSettings, ['accent_color', 'title', 'description', 'locale', 'icon']);
const [customColor, setCustomColor] = useState<{active: boolean}>({active: false});

if (labs) {
i18nEnabled = JSON.parse(labs).i18n;
}

useEffect(() => {
if (!siteData) {
return;
Expand All @@ -52,8 +46,7 @@ const EmbedSignupFormModal = NiceModal.create(() => {
},
labels: selectedLabels.map(({label}) => ({name: label})),
backgroundColor: selectedColor || '#08090c',
layout: selectedLayout,
i18nEnabled
layout: selectedLayout
};

const previewCode = generateCode({
Expand All @@ -67,7 +60,7 @@ const EmbedSignupFormModal = NiceModal.create(() => {
...defaultConfig
});
setGeneratedScript(generatedCode);
}, [siteData, accentColor, selectedLabels, config, title, selectedColor, selectedLayout, locale, i18nEnabled, icon, description]);
}, [siteData, accentColor, selectedLabels, config, title, selectedColor, selectedLayout, locale, icon, description]);

const handleCopyClick = async () => {
try {
Expand Down
11 changes: 3 additions & 8 deletions apps/admin-x-settings/src/utils/generate-embed-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ export type GenerateCodeOptions = {
icon?: string;
title?: string;
description?: string;
locale?: string;
locale: string;
};
labels: Array<{ name: string }>;
backgroundColor: string;
layout: string;
i18nEnabled: boolean;
};

type OptionsType = {
Expand All @@ -36,22 +35,18 @@ export const generateCode = ({
settings,
labels,
backgroundColor,
layout,
i18nEnabled
layout
}: GenerateCodeOptions) => {
const siteUrl = config.blogUrl;
const scriptUrl = config.signupForm.url.replace('{version}', config.signupForm.version);

let options: OptionsType = {
site: siteUrl,
locale: settings.locale,
'button-color': settings.accentColor,
'button-text-color': textColorForBackgroundColor(settings.accentColor).hex()
};

if (i18nEnabled && settings.locale) {
options.locale = settings.locale;
}

for (const [i, label] of labels.entries()) {
options[`label-${i + 1}`] = label.name;
}
Expand Down
24 changes: 9 additions & 15 deletions apps/admin-x-settings/test/unit/utils/generate-embed-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,57 +15,51 @@ describe('generateCode', function () {
}
},
settings: {
accentColor: '#000000'
accentColor: '#000000',
locale: 'af'
},
labels: [],
backgroundColor: '#000000',
layout: 'minimal',
i18nEnabled: false
layout: 'minimal'
};
});

it('generates a basic embed script', function () {
assert.equal(generateCode(genOptions), '<div style="min-height: 58px;max-width: 440px;margin: 0 auto;width: 100%"><script src="https://example.com" data-button-color="#000000" data-button-text-color="#FFFFFF" data-site="https://example.com" async></script></div>');
});

it('generates a basic embed script with i18n', function () {
genOptions.i18nEnabled = true;
genOptions.settings.locale = 'af';
assert.equal(generateCode(genOptions), '<div style="min-height: 58px;max-width: 440px;margin: 0 auto;width: 100%"><script src="https://example.com" data-button-color="#000000" data-button-text-color="#FFFFFF" data-site="https://example.com" data-locale="af" async></script></div>');
});

it('generates a basic embed script with labels', function () {
genOptions.labels = [{name: 'label1'}, {name: 'label2'}];
assert.equal(generateCode(genOptions), '<div style="min-height: 58px;max-width: 440px;margin: 0 auto;width: 100%"><script src="https://example.com" data-label-1="label1" data-label-2="label2" data-button-color="#000000" data-button-text-color="#FFFFFF" data-site="https://example.com" async></script></div>');
assert.equal(generateCode(genOptions), '<div style="min-height: 58px;max-width: 440px;margin: 0 auto;width: 100%"><script src="https://example.com" data-label-1="label1" data-label-2="label2" data-button-color="#000000" data-button-text-color="#FFFFFF" data-site="https://example.com" data-locale="af" async></script></div>');
});

it('generated an embed with an icon', function () {
genOptions.settings.icon = 'https://example.com/content/images/size/w256h256/2023/09/snoopy.png';
genOptions.layout = 'all-in-one';
assert.equal(generateCode(genOptions), '<div style="height: 40vmin;min-height: 360px"><script src="https://example.com" data-background-color="#000000" data-text-color="#FFFFFF" data-button-color="#000000" data-button-text-color="#FFFFFF" data-title="" data-description="" data-icon="https://example.com/content/images/size/w192h192/size/w256h256/2023/09/snoopy.png" data-site="https://example.com" async></script></div>');
assert.equal(generateCode(genOptions), '<div style="height: 40vmin;min-height: 360px"><script src="https://example.com" data-background-color="#000000" data-text-color="#FFFFFF" data-button-color="#000000" data-button-text-color="#FFFFFF" data-title="" data-description="" data-icon="https://example.com/content/images/size/w192h192/size/w256h256/2023/09/snoopy.png" data-site="https://example.com" data-locale="af" async></script></div>');
});

it('renders a full preview', function () {
genOptions.preview = true;
genOptions.layout = 'all-in-one';
assert.equal(generateCode(genOptions), '<div style="height: 100vh"><script src="https://example.com" data-background-color="#000000" data-text-color="#FFFFFF" data-button-color="#000000" data-button-text-color="#FFFFFF" data-title="" data-description="" data-site="https://example.com" async></script></div>');
assert.equal(generateCode(genOptions), '<div style="height: 100vh"><script src="https://example.com" data-background-color="#000000" data-text-color="#FFFFFF" data-button-color="#000000" data-button-text-color="#FFFFFF" data-title="" data-description="" data-site="https://example.com" data-locale="af" async></script></div>');
});

it('renders a preview with a minimal layout', function () {
genOptions.preview = true;
genOptions.layout = 'minimal';
assert.equal(generateCode(genOptions), '<div style="position: absolute; z-index: -1; top: 0; left: 0; width: 100%; height: 100%; background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%), linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%);background-size: 16px 16px;background-position: 0 0, 8px 8px;;"></div><div style="min-height: 58px; max-width: 440px;width: 100%;position: absolute; left: 50%; top:50%; transform: translate(-50%, -50%);"><script src="https://example.com" data-button-color="#000000" data-button-text-color="#FFFFFF" data-site="https://example.com" async></script></div>');
assert.equal(generateCode(genOptions), '<div style="position: absolute; z-index: -1; top: 0; left: 0; width: 100%; height: 100%; background-image: linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%), linear-gradient(45deg, #eee 25%, transparent 25%, transparent 75%, #eee 75%);background-size: 16px 16px;background-position: 0 0, 8px 8px;;"></div><div style="min-height: 58px; max-width: 440px;width: 100%;position: absolute; left: 50%; top:50%; transform: translate(-50%, -50%);"><script src="https://example.com" data-button-color="#000000" data-button-text-color="#FFFFFF" data-site="https://example.com" data-locale="af" async></script></div>');
});

it('generates text color based on background color - light background, black text', function () {
genOptions.backgroundColor = '#ffffff';
genOptions.layout = 'all-in-one';
assert.equal(generateCode(genOptions), '<div style="height: 40vmin;min-height: 360px"><script src="https://example.com" data-background-color="#ffffff" data-text-color="#000000" data-button-color="#000000" data-button-text-color="#FFFFFF" data-title="" data-description="" data-site="https://example.com" async></script></div>');
assert.equal(generateCode(genOptions), '<div style="height: 40vmin;min-height: 360px"><script src="https://example.com" data-background-color="#ffffff" data-text-color="#000000" data-button-color="#000000" data-button-text-color="#FFFFFF" data-title="" data-description="" data-site="https://example.com" data-locale="af" async></script></div>');
});

it('generates text color based on background color - black background, light text', function () {
genOptions.backgroundColor = '#000000';
genOptions.layout = 'all-in-one';
assert.equal(generateCode(genOptions), '<div style="height: 40vmin;min-height: 360px"><script src="https://example.com" data-background-color="#000000" data-text-color="#FFFFFF" data-button-color="#000000" data-button-text-color="#FFFFFF" data-title="" data-description="" data-site="https://example.com" async></script></div>');
assert.equal(generateCode(genOptions), '<div style="height: 40vmin;min-height: 360px"><script src="https://example.com" data-background-color="#000000" data-text-color="#FFFFFF" data-button-color="#000000" data-button-text-color="#FFFFFF" data-title="" data-description="" data-site="https://example.com" data-locale="af" async></script></div>');
});
});
2 changes: 1 addition & 1 deletion apps/comments-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryghost/comments-ui",
"version": "1.3.0",
"version": "1.3.1",
"license": "MIT",
"repository": "https://github.com/TryGhost/Ghost",
"author": "Ghost Foundation",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ const CommentingDisabledBox: React.FC = () => {

return (
<>
<h1 className="mb-[8px] text-center font-sans text-2xl font-semibold tracking-tight text-black dark:text-[rgba(255,255,255,0.85)]">
<h1 className="mb-2 text-center font-sans text-2xl font-semibold tracking-tight text-neutral-900 dark:text-white/85">
{t('Commenting disabled')}
</h1>
<p className="mb-[28px] w-full px-0 text-center font-sans text-lg leading-normal text-neutral-600 sm:max-w-screen-sm sm:px-8 dark:text-[rgba(255,255,255,0.85)]">
<p className="w-full text-balance text-center text-lg leading-normal text-neutral-900 sm:px-8 dark:text-white/85">
{supportEmail ? (
<>
{t('You can\'t post comments in this publication.')}{' '}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {Comment, useAppContext} from '../../../app-context';
import {Comment, useAppContext, useLabs} from '../../../app-context';

type Props = {
comment: Comment;
close: () => void;
};
const AdminContextMenu: React.FC<Props> = ({comment, close}) => {
const {dispatchAction, t} = useAppContext();
const {dispatchAction, t, adminUrl} = useAppContext();
const labs = useLabs();

const hideComment = () => {
dispatchAction('hideComment', comment);
Expand All @@ -18,6 +19,7 @@ const AdminContextMenu: React.FC<Props> = ({comment, close}) => {
};

const isHidden = comment.status !== 'published';
const adminCommentUrl = adminUrl ? `${adminUrl}#/comments/?id=is:${comment.id}` : null;

return (
<div className="flex w-full flex-col gap-0.5">
Expand All @@ -31,6 +33,18 @@ const AdminContextMenu: React.FC<Props> = ({comment, close}) => {
<span className="hidden sm:inline">{t('Hide comment')}</span><span className="sm:hidden">{t('Hide')}</span>
</button>
}
{labs?.commentModeration && adminCommentUrl && (
<a
className="w-full rounded px-2.5 py-1.5 text-left text-[14px] transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-700"
data-testid="view-in-admin-button"
href={adminCommentUrl}
rel="noopener noreferrer"
target="_blank"
onClick={close}
>
{t('View in admin')}
</a>
)}
</div>
);
};
Expand Down
49 changes: 49 additions & 0 deletions apps/comments-ui/test/e2e/admin-moderation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,53 @@ test.describe('Admin moderation', async () => {
const {frame} = await initializeTest(page);
await expect(frame.getByTestId('count')).toContainText('3 comments');
});

test.describe('View in admin link', () => {
test('shows View in admin link when commentModeration flag is enabled', async ({page}) => {
mockedApi.addComment({id: 'test-comment-id', html: '<p>This is a comment</p>'});

await mockAdminAuthFrame({page, admin});

const {frame} = await initialize({
mockedApi,
page,
publication: 'Publisher Weekly',
title: 'Member discussion',
count: true,
admin,
labs: {
commentModeration: true
}
});

const moreButtons = frame.getByTestId('more-button');
await moreButtons.nth(0).click();

const viewInAdminLink = frame.getByTestId('view-in-admin-button');
await expect(viewInAdminLink).toBeVisible();
await expect(viewInAdminLink).toHaveAttribute('href', `${admin}#/comments/?id=is:test-comment-id`);
await expect(viewInAdminLink).toHaveAttribute('target', '_blank');
});

test('hides View in admin link when commentModeration flag is not set', async ({page}) => {
mockedApi.addComment({html: '<p>This is a comment</p>'});

await mockAdminAuthFrame({page, admin});

const {frame} = await initialize({
mockedApi,
page,
publication: 'Publisher Weekly',
title: 'Member discussion',
count: true,
admin,
labs: {}
});

const moreButtons = frame.getByTestId('more-button');
await moreButtons.nth(0).click();

await expect(frame.getByTestId('view-in-admin-button')).not.toBeVisible();
});
});
});
2 changes: 1 addition & 1 deletion apps/portal/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tryghost/portal",
"version": "2.58.0",
"version": "2.58.1",
"license": "MIT",
"repository": "https://github.com/TryGhost/Ghost",
"author": "Ghost Foundation",
Expand Down
7 changes: 2 additions & 5 deletions apps/portal/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ export default class App extends React.Component {
lastPage: null,
customSiteUrl: props.customSiteUrl,
locale: props.locale,
scrollbarWidth: 0,
labs: props.labs || {}
scrollbarWidth: 0
};
}

Expand Down Expand Up @@ -110,7 +109,6 @@ export default class App extends React.Component {
siteUrl,
site: contextState.site,
member: contextState.member,
labs: contextState.labs,
doAction: contextState.doAction,
captureException: Sentry.captureException
});
Expand Down Expand Up @@ -964,7 +962,7 @@ export default class App extends React.Component {

/**Get final App level context from App state*/
getContextFromState() {
const {site, member, offers, action, actionErrorMessage, page, lastPage, showPopup, pageQuery, pageData, popupNotification, customSiteUrl, dir, scrollbarWidth, labs, otcRef, sniperLinks} = this.state;
const {site, member, offers, action, actionErrorMessage, page, lastPage, showPopup, pageQuery, pageData, popupNotification, customSiteUrl, dir, scrollbarWidth, otcRef, sniperLinks} = this.state;
const contextPage = this.getContextPage({site, page, member});
const contextMember = this.getContextMember({page: contextPage, member, customSiteUrl});
return {
Expand All @@ -984,7 +982,6 @@ export default class App extends React.Component {
customSiteUrl,
dir,
scrollbarWidth,
labs,
otcRef,
sniperLinks,
doAction: (_action, data) => this.dispatchAction(_action, data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,28 @@ function FreeTrialLabel({subscription}) {
return null;
}

/**
* Display discounted price if an offer is active
*
* Examples:
* - "$10.00 — Next payment" (once offer)
* - "$10.00/month — Forever" (forever offer)
* - "$10.00/month — Ends 2026-01-01" (repeating offer)
*
* @param {Object} nextPayment
* @param {number} nextPayment.originalAmount - Original amount
* @param {number} nextPayment.amount - Amount after discount. Same as original amount if no discount.
* @param {string} nextPayment.currency - Currency (e.g. USD, EUR)
* @param {'month'|'year'} nextPayment.interval
* @param {Object|null} nextPayment.discount
* @param {'once'|'repeating'|'forever'} nextPayment.discount.duration
* @param {string} nextPayment.discount.start - Discount start date (ISO 8601 date string)
* @param {string|null} nextPayment.discount.end - Discount end date (ISO 8601 date string), null for forever / once offers
* @param {'fixed'|'percent'} nextPayment.discount.type
* @param {number} nextPayment.discount.amount - Discount amount (e.g. 20 for 20% percent offer, or 2 for $2 fixed offer)

* @returns {string}
*/
function getOfferLabel({nextPayment}) {
if (!nextPayment) {
return '';
Expand All @@ -202,14 +224,21 @@ function getOfferLabel({nextPayment}) {
if (discount.duration === 'forever') {
durationLabel = t('Forever');
} else if (discount.duration === 'once') {
durationLabel = 'Next payment';
durationLabel = t('Next payment');
} else if (discount.duration === 'repeating' && discount.end) {
durationLabel = t('Ends {offerEndDate}', {offerEndDate: getDateString(discount.end)});
}

// Format the discounted price from next_payment.amount
const formattedPrice = Intl.NumberFormat('en', {currency: nextPayment.currency, style: 'currency'}).format(nextPayment.amount / 100);
return `${formattedPrice}/${nextPayment.interval}${durationLabel ? ` — ${durationLabel}` : ''}`;

let displayedPrice = '';
if (discount.duration === 'once') {
displayedPrice = formattedPrice;
} else {
displayedPrice = `${formattedPrice}/${nextPayment.interval}`;
}

return `${displayedPrice}${durationLabel ? ` — ${durationLabel}` : ''}`;
}

export default PaidAccountActions;
Loading
Loading