Skip to content

[pull] main from TryGhost:main #434

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 6, 2025
2 changes: 1 addition & 1 deletion apps/comments-ui/src/components/content/forms/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ const FormHeader: React.FC<FormHeaderProps> = ({show, name, expertise, replyingT
type="button"
onMouseDown={editExpertise}
>
<span><span className="mx-[0.3em] hidden sm:inline">·</span>{expertise ? expertise : 'Add your expertise'}</span>
<span><span className="mx-[0.3em] hidden sm:inline">·</span>{expertise ? expertise : t('Add your expertise')}</span>
{expertise && <EditIcon className="ml-1 h-[12px] w-[12px] translate-x-[-6px] stroke-black/50 opacity-0 transition-all duration-100 ease-out group-hover:translate-x-0 group-hover:stroke-black/75 group-hover:opacity-100 dark:stroke-white/60 dark:group-hover:stroke-white/75" />}
</button>
</div>
Expand Down
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.50.7",
"version": "2.50.8",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
17 changes: 12 additions & 5 deletions apps/portal/src/components/common/Switch.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {useContext, useEffect, useState} from 'react';
import AppContext from '../../AppContext';
import {useEffect, useRef, useState} from 'react';

export const SwitchStyles = `
.gh-portal-for-switch label,
Expand Down Expand Up @@ -80,16 +79,24 @@ export const SwitchStyles = `
`;

function Switch({id, label = '', onToggle, checked = false, dataTestId = 'switch-input'}) {
const {action} = useContext(AppContext);
const [isChecked, setIsChecked] = useState(checked);
const isActionChanged = ['updateNewsletter:failed', 'updateNewsletter:success'].includes(action);

useEffect(() => {
setIsChecked(checked);
}, [checked, isActionChanged]);
}, [checked]);

const inputRef = useRef(null);
useEffect(() => {
if (inputRef.current && inputRef.current.checked !== isChecked) {
inputRef.current.checked = isChecked;
}
}, [isChecked, id]);

return (
<div className="gh-portal-for-switch" data-test-switch={dataTestId}>
<label className="switch" htmlFor={id}>
<input
ref={inputRef}
type="checkbox"
checked={isChecked}
id={id}
Expand Down
33 changes: 31 additions & 2 deletions apps/posts/src/hooks/usePostReferrers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
import moment from 'moment';
import {useMemo} from 'react';
import {usePostGrowthStats as usePostGrowthStatsAPI, usePostReferrers as usePostReferrersAPI} from '@tryghost/admin-x-framework/api/stats';

export const usePostReferrers = (postId: string) => {
const {data: postReferrerResponse, isLoading: isPostReferrersLoading} = usePostReferrersAPI(postId);
// Helper function to convert range to date parameters
export const getRangeDates = (rangeInDays: number) => {
// Always use UTC to stay aligned with the backend's date arithmetic
const endDate = moment.utc().format('YYYY-MM-DD');
let dateFrom;

if (rangeInDays === 1) {
// Today
dateFrom = endDate;
} else if (rangeInDays === 1000) {
// All time - use a far past date
dateFrom = '2010-01-01';
} else {
// Specific range
// Guard against invalid ranges
const safeRange = Math.max(1, rangeInDays);
dateFrom = moment.utc().subtract(safeRange - 1, 'days').format('YYYY-MM-DD');
}

return {dateFrom, endDate};
};

export const usePostReferrers = (postId: string, range: number) => {
const {dateFrom} = useMemo(() => getRangeDates(range), [range]);
const {data: postReferrerResponse, isLoading: isPostReferrersLoading} = usePostReferrersAPI(postId, {
searchParams: {
date_from: dateFrom
}
});
// API doesn't support date_from yet, so we fetch all data and filter on the client for now
const {data: postGrowthStatsResponse, isLoading: isPostGrowthStatsLoading} = usePostGrowthStatsAPI(postId);

const stats = useMemo(() => postReferrerResponse?.stats || [], [postReferrerResponse]);
Expand Down
9 changes: 5 additions & 4 deletions apps/posts/src/views/PostAnalytics/Growth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import PostAnalyticsContent from './components/PostAnalyticsContent';
import PostAnalyticsHeader from './components/PostAnalyticsHeader';
import PostAnalyticsLayout from './layout/PostAnalyticsLayout';
import {Card, CardContent, CardDescription, CardHeader, CardTitle, LucideIcon, Separator, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, ViewHeader, ViewHeaderActions, formatNumber} from '@tryghost/shade';
import {useGlobalData} from '@src/providers/GlobalDataProvider';
import {useParams} from '@tryghost/admin-x-framework';
import {usePostReferrers} from '../../hooks/usePostReferrers';

const STATS_DEFAULT_SOURCE_ICON_URL = 'https://static.ghost.org/v5.0.0/images/globe-icon.svg';

interface postAnalyticsProps {}

const Growth: React.FC<postAnalyticsProps> = () => {
// const {isLoading: isConfigLoading} = useGlobalData();
const {postId} = useParams();
const {stats: postReferrers, totals, isLoading} = usePostReferrers(postId || '');

// const {range} = useGlobalData();
const {range} = useGlobalData();
const {stats: postReferrers, totals, isLoading} = usePostReferrers(postId || '', range);

return (
<PostAnalyticsLayout>
Expand Down Expand Up @@ -92,7 +93,7 @@ const Growth: React.FC<postAnalyticsProps> = () => {
</Table>
:
<div className='py-20 text-center text-sm text-gray-700'>
No source data available for this post.
Once someone signs up on this post, sources will show here.
</div>
}
</CardContent>
Expand Down
20 changes: 7 additions & 13 deletions apps/posts/src/views/PostAnalytics/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import React from 'react';
import {LucideIcon, RightSidebarMenu, RightSidebarMenuLink} from '@tryghost/shade';
import {getSettingValue} from '@tryghost/admin-x-framework/api/settings';
import {useGlobalData} from '@src/providers/GlobalDataProvider';
import {useLocation, useNavigate, useParams} from '@tryghost/admin-x-framework';

const Sidebar:React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const {postId} = useParams();
const {settings} = useGlobalData();
const labs = JSON.parse(getSettingValue<string>(settings, 'labs') || '{}');

return (
<div className='grow border-l py-8 pl-6 pr-0'>
Expand All @@ -26,17 +22,15 @@ const Sidebar:React.FC = () => {
<LucideIcon.MousePointer size={16} strokeWidth={1.25} />
Web
</RightSidebarMenuLink>
{labs.trafficAnalyticsAlpha &&
<RightSidebarMenuLink active={location.pathname === `/analytics/${postId}/growth`} onClick={() => {
navigate(`/analytics/${postId}/growth`);
}}>
<LucideIcon.Sprout size={16} strokeWidth={1.25} />
Growth
</RightSidebarMenuLink>
}
<RightSidebarMenuLink active={location.pathname === `/analytics/${postId}/growth`} onClick={() => {
navigate(`/analytics/${postId}/growth`);
}}>
<LucideIcon.Sprout size={16} strokeWidth={1.25} />
Growth
</RightSidebarMenuLink>
</RightSidebarMenu>
</div>
);
};

export default Sidebar;
export default Sidebar;
23 changes: 14 additions & 9 deletions apps/stats/src/views/Stats/Growth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@ import StatsView from './layout/StatsView';
import {Button, Card, CardContent, CardDescription, CardHeader, CardTitle, ChartConfig, ChartContainer, ChartTooltip, H1, Recharts, Separator, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Tabs, TabsList, ViewHeader, ViewHeaderActions, formatDisplayDate, formatNumber} from '@tryghost/shade';
import {DiffDirection, useGrowthStats} from '@src/hooks/useGrowthStats';
import {KpiTabTrigger, KpiTabValue} from './components/KpiTab';
import {Navigate, useNavigate} from '@tryghost/admin-x-framework';
import {calculateYAxisWidth, getYRange, getYTicks, sanitizeChartData} from '@src/utils/chart-helpers';
import {getSettingValue} from '@tryghost/admin-x-framework/api/settings';
import {useGlobalData} from '@src/providers/GlobalDataProvider';
import {useNavigate} from '@tryghost/admin-x-framework';
import {useTopPostsStatsWithRange} from '@src/hooks/useTopPostsStatsWithRange';

// TODO: Move to @tryghost/shade
Expand Down Expand Up @@ -56,8 +55,7 @@ const GrowthKPIs: React.FC<{
totals: Totals;
}> = ({chartData: allChartData, totals}) => {
const [currentTab, setCurrentTab] = useState('total-members');
const {settings, range} = useGlobalData();
const labs = JSON.parse(getSettingValue<string>(settings, 'labs') || '{}');
const {range} = useGlobalData();

const {totalMembers, freeMembers, paidMembers, mrr, percentChanges, directions} = totals;

Expand Down Expand Up @@ -128,10 +126,6 @@ const GrowthKPIs: React.FC<{
return processedData;
}, [currentTab, allChartData, range]);

if (!labs.trafficAnalyticsAlpha) {
return <Navigate to='/' />;
}

const chartConfig = {
value: {
label: currentTab === 'mrr' ? 'MRR' : 'Members'
Expand Down Expand Up @@ -242,7 +236,18 @@ const GrowthKPIs: React.FC<{
}}
tickLine={false}
ticks={getYTicks(chartData)}
width={calculateYAxisWidth(getYTicks(chartData), formatNumber)}
width={calculateYAxisWidth(getYTicks(chartData), (value) => {
switch (currentTab) {
case 'total-members':
case 'free-members':
case 'paid-members':
return formatNumber(value);
case 'mrr':
return `$${value}`;
default:
return value.toLocaleString();
}
})}
/>
<ChartTooltip
content={<CustomTooltipContent range={range} />}
Expand Down
20 changes: 7 additions & 13 deletions apps/stats/src/views/Stats/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import React from 'react';
import {LucideIcon, RightSidebarMenu, RightSidebarMenuLink} from '@tryghost/shade';
import {getSettingValue} from '@tryghost/admin-x-framework/api/settings';
import {useGlobalData} from '@src/providers/GlobalDataProvider';
import {useLocation, useNavigate} from '@tryghost/admin-x-framework';

const Sidebar:React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const {settings} = useGlobalData();
const labs = JSON.parse(getSettingValue<string>(settings, 'labs') || '{}');

return (
<div className='grow border-l px-6 py-8'>
Expand All @@ -33,17 +29,15 @@ const Sidebar:React.FC = () => {
Locations
</RightSidebarMenuLink>

{labs.trafficAnalyticsAlpha &&
<RightSidebarMenuLink active={location.pathname === '/growth/'} onClick={() => {
navigate('/growth/');
}}>
<LucideIcon.Sprout size={16} strokeWidth={1.25} />
Growth
</RightSidebarMenuLink>
}
<RightSidebarMenuLink active={location.pathname === '/growth/'} onClick={() => {
navigate('/growth/');
}}>
<LucideIcon.Sprout size={16} strokeWidth={1.25} />
Growth
</RightSidebarMenuLink>
</RightSidebarMenu>
</div>
);
};

export default Sidebar;
export default Sidebar;
3 changes: 2 additions & 1 deletion ghost/core/core/server/services/stats/PostsStatsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class PostsStatsService {
.from('posts as p')
.leftJoin('free', 'p.id', 'free.post_id')
.leftJoin('paid', 'p.id', 'paid.post_id')
.leftJoin('mrr', 'p.id', 'mrr.post_id');
.leftJoin('mrr', 'p.id', 'mrr.post_id')
.where('p.status', 'published');

const results = await query
.orderBy(orderField, orderDirection)
Expand Down
6 changes: 4 additions & 2 deletions ghost/core/test/unit/server/services/stats/posts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ describe('PostsStatsService', function () {
let memberIdCounter = 0;
let subscriptionIdCounter = 0;

async function _createPost(id, title) {
await db('posts').insert({id, title});
async function _createPost(id, title, status = 'published') {
await db('posts').insert({id, title, status});
}

async function _createFreeSignupEvent(postId, memberId, referrerSource, createdAt = new Date()) {
Expand Down Expand Up @@ -116,6 +116,7 @@ describe('PostsStatsService', function () {
await db.schema.createTable('posts', function (table) {
table.string('id').primary();
table.string('title');
table.string('status');
});

await db.schema.createTable('members_created_events', function (table) {
Expand Down Expand Up @@ -157,6 +158,7 @@ describe('PostsStatsService', function () {
await _createPost('post2', 'Post 2');
await _createPost('post3', 'Post 3');
await _createPost('post4', 'Post 4');
await _createPost('post5', 'Post 5', 'draft');
});

afterEach(async function () {
Expand Down
1 change: 1 addition & 0 deletions ghost/i18n/locales/af/comments.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Add comment": "Voeg kommentaar by",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "Voeg konteks by jou kommentaar, deel jou naam en kundigheid om 'n gesonde bespreking te bevorder.",
"Add reply": "Voeg antwoord by",
"Add your expertise": "",
"Already a member?": "Reeds 'n lid?",
"Anonymous": "Anoniem",
"Are you sure?": "",
Expand Down
1 change: 1 addition & 0 deletions ghost/i18n/locales/ar/comments.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Add comment": "اضف تعليق",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "قم بإضافة سياق لتعليقك، شارك اسمك و مجال خبرتك لكي نرعى محادثة مثمرة",
"Add reply": "ردّ",
"Add your expertise": "",
"Already a member?": "هل انت مسجل بالفعل؟",
"Anonymous": "مجهول",
"Are you sure?": "",
Expand Down
1 change: 1 addition & 0 deletions ghost/i18n/locales/bg/comments.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Add comment": "Нов коментар",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "Добавете контекст към коментара си, споделете името си и опита си, за да насърчите полезна дискусия.",
"Add reply": "Отговор",
"Add your expertise": "",
"Already a member?": "Вече сте абонат?",
"Anonymous": "Анонимен",
"Are you sure?": "Убедени ли сте?",
Expand Down
1 change: 1 addition & 0 deletions ghost/i18n/locales/bn/comments.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Add comment": "মন্তব্য যোগ করুন",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "আপনার মন্তব্যে প্রসঙ্গ যোগ করুন, একটি সুস্থ আলোচনা করতে আপনার নাম ও অভিজ্ঞতা শেয়ার করুন।",
"Add reply": "উত্তর যোগ করুন",
"Add your expertise": "",
"Already a member?": "ইত:পূর্বে সদস্য?",
"Anonymous": "অজ্ঞাতনামা",
"Are you sure?": "",
Expand Down
1 change: 1 addition & 0 deletions ghost/i18n/locales/bs/comments.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Add comment": "Dodaj komentar",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "Dodaj kontekst svom komentaru, podijeli svoje ime i stručnost u svrhu kvalitetnije diskusije.",
"Add reply": "Objavi komentar",
"Add your expertise": "",
"Already a member?": "Već si član?",
"Anonymous": "Anoniman",
"Are you sure?": "",
Expand Down
1 change: 1 addition & 0 deletions ghost/i18n/locales/ca/comments.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Add comment": "Afegeix un comentari",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "Afegeix context al teu comentari, comparteix el teu nom i experiència per fomentar una discussió sana.",
"Add reply": "Afegeix una resposta",
"Add your expertise": "",
"Already a member?": "Ja n'ets membre?",
"Anonymous": "Anònim",
"Are you sure?": "N'estàs segur?",
Expand Down
3 changes: 2 additions & 1 deletion ghost/i18n/locales/context.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"Add comment": "Button text to post a comment",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "Invitation to include additional info when commenting",
"Add reply": "Button text to post your reply",
"Add your expertise": "A link in the comments to add ones expertise if not yet provided",
"After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Confirmation message explaining how free trials work during signup",
"All the best!": "A light-hearted ending to an email",
"Already a member?": "A link displayed on signup screen, inviting people to log in if they have already signed up previously",
Expand Down Expand Up @@ -348,4 +349,4 @@
"{{memberEmail}} will no longer receive this newsletter.": "A message shown when a user unsubscribes from a newsletter",
"{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "A message shown when a user unsubscribes from a newsletter",
"{{trialDays}} days free": "A label for free trial days"
}
}
1 change: 1 addition & 0 deletions ghost/i18n/locales/cs/comments.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Add comment": "Přidat komentář",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "Pro podporu zdravé diskuse přidejte ke svému komentáři kontext, své jméno a odbornost.",
"Add reply": "Přidat odpověď",
"Add your expertise": "",
"Already a member?": "Už jste členem?",
"Anonymous": "Anonymní",
"Are you sure?": "",
Expand Down
1 change: 1 addition & 0 deletions ghost/i18n/locales/da/comments.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Add comment": "Tilføj kommentar",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "Tilføj kontekst til din kommentar, del dit navn og ekspertise for at fremme en sund diskussion.",
"Add reply": "Tilføj svar",
"Add your expertise": "",
"Already a member?": "Allerede medlem?",
"Anonymous": "Anonym",
"Are you sure?": "Er du sikker?",
Expand Down
1 change: 1 addition & 0 deletions ghost/i18n/locales/de-CH/comments.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Add comment": "Kommentar hinzufügen",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "Fügen Sie Kontext zu Ihrem Kommentar hinzu, teilen Sie Ihren Namen und Ihre Expertise, um eine gesunde Diskussion zu fördern.",
"Add reply": "Antwort hinzufügen",
"Add your expertise": "",
"Already a member?": "Bereits ein Mitglied?",
"Anonymous": "Anonym",
"Are you sure?": "Sind Sie sicher?",
Expand Down
1 change: 1 addition & 0 deletions ghost/i18n/locales/de/comments.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Add comment": "Kommentar hinzufügen",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "Füge deinem Kommentar Kontext hinzu, teile deinen Namen und deine Expertise, um eine gesunde Diskussion zu fördern.",
"Add reply": "Antwort hinzufügen",
"Add your expertise": "Ergänze deine Expertise",
"Already a member?": "Bist du bereits Mitglied?",
"Anonymous": "Anonym",
"Are you sure?": "Bist du sicher?",
Expand Down
1 change: 1 addition & 0 deletions ghost/i18n/locales/el/comments.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Add comment": "Προσθήκη σχολίου",
"Add context to your comment, share your name and expertise to foster a healthy discussion.": "Προσθέστε περιεχόμενο στο σχόλιό σας, μοιραστείτε το όνομα και την εξειδίκευσή σας για να προωθήσετε μια υγιή συζήτηση.",
"Add reply": "Προσθήκη απάντησης",
"Add your expertise": "",
"Already a member?": "Ήδη μέλος;",
"Anonymous": "Ανώνυμος",
"Are you sure?": "",
Expand Down
Loading
Loading