Skip to content
Open
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
160 changes: 160 additions & 0 deletions apps/posts/src/views/members/components/members-empty-state.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import React, {useCallback, useState} from 'react';
import {Button, EmptyIndicator} from '@tryghost/shade/components';
import {LucideIcon} from '@tryghost/shade/utils';
import {getGhostPaths} from '@tryghost/admin-x-framework/helpers';
import {getSettingValue, useBrowseSettings} from '@tryghost/admin-x-framework/api/settings';
import {toast} from 'sonner';
import {useCurrentUser} from '@tryghost/admin-x-framework/api/current-user';
import {useNavigate} from '@tryghost/admin-x-framework';

const MembersEmptyState: React.FC<{onMemberCreated?: () => void}> = ({onMemberCreated}) => {
const {data: settingsData} = useBrowseSettings({});
const {data: currentUser} = useCurrentUser();
const navigate = useNavigate();
const [isAdding, setIsAdding] = useState(false);

const {assetRoot} = getGhostPaths();
const membersSignupAccess = getSettingValue<string>(settingsData?.settings ?? null, 'members_signup_access');
const membershipsEnabled = membersSignupAccess !== 'none';
Comment on lines +11 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t treat unresolved settings as memberships enabled.

While useBrowseSettings is still loading, membersSignupAccess is undefined, so undefined !== 'none' renders the enabled CTA even on sites where memberships are disabled. Gate the branch until settings are loaded.

Proposed fix
-import {Button, EmptyIndicator} from '@tryghost/shade/components';
+import {Button, EmptyIndicator, LoadingIndicator} from '@tryghost/shade/components';
@@
-    const {data: settingsData} = useBrowseSettings({});
+    const {data: settingsData, isLoading: isSettingsLoading} = useBrowseSettings({});
@@
     const {assetRoot} = getGhostPaths();
     const membersSignupAccess = getSettingValue<string>(settingsData?.settings ?? null, 'members_signup_access');
     const membershipsEnabled = membersSignupAccess !== 'none';
+
+    if (isSettingsLoading || !settingsData?.settings) {
+        return (
+            <div className="flex h-full items-center justify-center px-4">
+                <LoadingIndicator size="lg" />
+            </div>
+        );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const {data: settingsData} = useBrowseSettings({});
const {data: currentUser} = useCurrentUser();
const navigate = useNavigate();
const [isAdding, setIsAdding] = useState(false);
const {assetRoot} = getGhostPaths();
const membersSignupAccess = getSettingValue<string>(settingsData?.settings ?? null, 'members_signup_access');
const membershipsEnabled = membersSignupAccess !== 'none';
const {data: settingsData, isLoading: isSettingsLoading} = useBrowseSettings({});
const {data: currentUser} = useCurrentUser();
const navigate = useNavigate();
const [isAdding, setIsAdding] = useState(false);
const {assetRoot} = getGhostPaths();
const membersSignupAccess = getSettingValue<string>(settingsData?.settings ?? null, 'members_signup_access');
const membershipsEnabled = membersSignupAccess !== 'none';
if (isSettingsLoading || !settingsData?.settings) {
return (
<div className="flex h-full items-center justify-center px-4">
<LoadingIndicator size="lg" />
</div>
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/posts/src/views/members/components/members-empty-state.tsx` around lines
11 - 18, The code treats memberships as enabled when settings are unresolved
because membersSignupAccess becomes undefined; update the membershipsEnabled
calculation to only evaluate after settings are loaded (e.g., check settingsData
or settingsData.settings exists) — for example in the component that calls
useBrowseSettings, change the membershipsEnabled derivation (currently using
membersSignupAccess and membershipsEnabled) to first gate on settingsData (or a
loading flag) and only set membershipsEnabled =
getSettingValue<string>(settingsData.settings, 'members_signup_access') !==
'none' when settingsData is present so the CTA is not shown while settings are
still loading.


const handleAddYourself = useCallback(async () => {
if (!currentUser || isAdding) {
return;
}

setIsAdding(true);
try {
const {apiRoot} = getGhostPaths();
const response = await fetch(`${apiRoot}/members/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
members: [{
email: currentUser.email,
name: currentUser.name
}]
})
});
Comment on lines +27 to +39
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect existing member-create/Admin API request conventions without modifying the repo.

set -euo pipefail

echo "Existing member API helpers or mutations:"
rg -n -C3 --type=ts --type=tsx '\b(use(Add|Create|Edit|Update)Member|addMember|createMember|createMembers|membersApi)\b' || true

echo
echo "Existing direct calls to the members endpoint:"
rg -n -C4 --type=ts --type=tsx 'fetch\s*\([^)]*/members/|apiRoot[^;\n]*/members/' || true

echo
echo "Admin API version/header conventions:"
rg -n -C3 --type=ts --type=tsx 'Accept-Version|Content-Type.*application/json|apiRoot' || true

Repository: TryGhost/Ghost

Length of output: 287


🏁 Script executed:

#!/bin/bash
# Search for member-related API helpers and mutations with corrected syntax

echo "=== Member API helpers/mutations ==="
rg -n -C2 '(useAddMember|useCreateMember|useEditMember|useUpdateMember|addMember|createMember|createMembers|membersApi)' --max-count=20 || true

echo
echo "=== Existing fetch calls to members endpoint ==="
rg -n -C3 '(fetch.*members|members.*fetch)' --max-count=20 || true

echo
echo "=== Admin API client usage patterns ==="
rg -n -C3 '(GhostAdminAPI|adminAPI|apiClient|useAdminAPI|useGhost)' apps/posts/src --max-count=30 || true

echo
echo "=== Looking at the specific file ==="
head -50 apps/posts/src/views/members/components/members-empty-state.tsx || true

Repository: TryGhost/Ghost

Length of output: 50373


🏁 Script executed:

#!/bin/bash
# Look at the specific file mentioned in the review
echo "=== File: members-empty-state.tsx ==="
cat -n apps/posts/src/views/members/components/members-empty-state.tsx

echo
echo "=== Search for Admin API/fetch patterns in apps/posts ==="
rg -n 'fetch|apiRoot|useGhost|adminAPI' apps/posts/src --max-count=30 -A 2

echo
echo "=== GhostAdminApiAdapter definition ==="
find . -name '*adapter*' -o -name '*Adapter*' | head -20 | xargs grep -l 'GhostAdminApiAdapter' 2>/dev/null || echo "No direct hits"
rg -n 'class GhostAdminApiAdapter|export.*GhostAdminApiAdapter' --max-count=10

Repository: TryGhost/Ghost

Length of output: 25068


🏁 Script executed:

#!/bin/bash
# Search for member API patterns in the framework
echo "=== Member-related API code in admin-x-framework ==="
find . -path "*/admin-x-framework*" -name "*.ts" -o -name "*.tsx" | xargs rg -l "member" 2>/dev/null | head -20

echo
echo "=== Check what the framework provides for API calls ==="
rg -n 'fetch|apiRoot' packages/@tryghost/admin-x-framework --max-count=30 -A 2 2>/dev/null || echo "Searching in framework..."

echo
echo "=== Look for API client patterns in admin-x-framework ==="
rg -n 'class.*Api|function.*Api|export.*api' packages/@tryghost/admin-x-framework/api --max-count=20 2>/dev/null || true

echo
echo "=== Check existing Admin API calls for headers/versioning ==="
rg -n 'Accept-Version|X-Ghost|authorization' packages/@tryghost/admin-x-framework --max-count=20 || true

Repository: TryGhost/Ghost

Length of output: 1643


🏁 Script executed:

#!/bin/bash
# Search for useBrowseMembersInfinite and other member hooks
echo "=== Member browse hooks ==="
rg -n 'useBrowseMembersInfinite|useMember|useEditMember|useCreateMember' --max-count=20

echo
echo "=== Check the hook implementation ==="
rg -n 'export.*useBrowseMembersInfinite' -A 10 --max-count=5

echo
echo "=== Compare: settings API usage in members-empty-state ==="
grep -n 'useBrowseSettings\|useCurrentUser' apps/posts/src/views/members/components/members-empty-state.tsx

echo
echo "=== Check framework package.json to understand dependencies ==="
cat package.json | grep -A 5 '@tryghost/admin-x-framework' || echo "Check if it's in workspace"

echo
echo "=== Look for member mutation/add operations in the posts app ==="
rg -n 'members' apps/posts/src --type=ts -g '*.ts' --max-count=20 -B 2 | grep -E '(hook|function|const.*=.*\(|mutation|add|create)' || true

Repository: TryGhost/Ghost

Length of output: 12417


🏁 Script executed:

#!/bin/bash
# Look at the full members.ts API file to see all exports
echo "=== Full members.ts API file ==="
cat apps/admin-x-framework/src/api/members.ts | head -200

echo
echo "=== Check for any create/add/mutation functions ==="
rg -n 'export.*create|export.*add|export.*mutation' apps/admin-x-framework/src/api/members.ts

echo
echo "=== Check how other mutations are done in the framework ==="
rg -n 'useMutation|createMutation' apps/admin-x-framework/src/api --max-count=20

Repository: TryGhost/Ghost

Length of output: 16926


🏁 Script executed:

#!/bin/bash
# Look at createMutation implementation
echo "=== createMutation implementation ==="
cat apps/admin-x-framework/src/utils/api/hooks.ts | head -300

echo
echo "=== Check for authentication/header handling ==="
rg -n 'Authorization|Accept-Version|headers' apps/admin-x-framework/src/utils/api --max-count=30 -A 2

echo
echo "=== Check if bare fetch in import-members-modal includes auth ==="
grep -A 20 'fetch.*members/upload' apps/posts/src/views/members/components/bulk-action-modals/import-members-modal.tsx

Repository: TryGhost/Ghost

Length of output: 14149


🏁 Script executed:

#!/bin/bash
# Check if members endpoint requires auth
echo "=== Check Admin API documentation/tests for members endpoint auth ==="
rg -n 'POST.*members' ghost/core/tests/api --max-count=10 -B 2 -A 2 | head -50

echo
echo "=== Verify if there are any tests for creating members ==="
rg -n 'create.*member\|add.*member' ghost/core/tests/api/members --max-count=15

Repository: TryGhost/Ghost

Length of output: 334


Use the framework's API abstraction or match its authentication headers.

This bare fetch call is missing critical headers that the framework normally provides. Specifically, it lacks credentials: 'include' (required for authentication), 'app-pragma': 'no-cache', and potential version headers. See import-members-modal.tsx for the correct bare-fetch pattern, or better yet, create a useCreateMembers mutation hook in the framework (similar to useAddTier, useAddNewsletter, etc.) to keep API concerns centralized.

Reference: Current bare fetch
const response = await fetch(`${apiRoot}/members/`, {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({...})
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/posts/src/views/members/components/members-empty-state.tsx` around lines
27 - 39, The POST to `${apiRoot}/members/` in this component is missing the
framework's required fetch settings (e.g., credentials and cache/version
headers); update the call to include credentials: 'include' and add headers like
'app-pragma': 'no-cache' and the app/version header matching the pattern in
import-members-modal.tsx, or better yet, extract this logic into a new mutation
hook (e.g., useCreateMembers) mirroring existing hooks such as
useAddTier/useAddNewsletter so API auth/headers are centralized and reusable;
modify the code that calls getGhostPaths() and currentUser to use that hook or
the corrected fetch options.


if (!response.ok) {
throw new Error('Failed to create member');
}

toast.success('You\'ve been added as a member');
onMemberCreated?.();
} catch {
toast.error('Failed to add member', {
description: 'There was a problem adding you as a member. Please try again.'
});
} finally {
setIsAdding(false);
}
}, [currentUser, isAdding, onMemberCreated]);

if (!membershipsEnabled) {
return (
<div className="flex h-full flex-col items-center justify-center px-4">
<EmptyIndicator
actions={
<Button variant="outline" asChild>
<a href="#/settings/members">Membership settings</a>
</Button>
}
description="Adjust your membership settings to start adding members."
title="Memberships have been disabled"
>
<LucideIcon.Users />
</EmptyIndicator>
</div>
);
}

return (
<div className="flex h-full flex-col items-center justify-center px-4">
<div className="flex max-w-lg flex-col items-center gap-3">
<EmptyIndicator
actions={
<div className="flex flex-col items-center gap-3">
<Button
disabled={isAdding}
onClick={handleAddYourself}
>
{isAdding ? 'Adding...' : 'Add yourself as a member to test'}
</Button>
<p className="text-sm text-muted-foreground">
Have members already?{' '}
<a className="font-medium text-foreground hover:underline" href="#/members/new">Add them manually</a>
{' '}or{' '}
<button
className="font-medium text-foreground hover:underline"
type="button"
onClick={() => navigate('/members/import')}
>
import from CSV
</button>
</p>
</div>
}
description="Use memberships to allow your readers to sign up and subscribe to your content."
title="Start building your audience"
>
<LucideIcon.Users />
</EmptyIndicator>

<div className="mt-4 grid w-full grid-cols-1 gap-4 sm:grid-cols-2">
<a
className="group flex flex-col overflow-hidden rounded-xl border bg-card transition-all hover:shadow-sm"
href="https://ghost.org/resources/build-audience-subscriber-signups/"
rel="noopener noreferrer"
target="_blank"
>
<div
className="h-36 w-full bg-cover bg-center"
style={{backgroundImage: `url(${assetRoot}img/marketing/members-1.jpg)`}}
/>
<div className="flex grow flex-col p-5">
<h4 className="text-sm font-semibold">
Building your audience with subscriber signups
</h4>
<p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">
Learn how to turn anonymous visitors into logged-in members with memberships in Ghost.
</p>
<span className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-foreground">
Start building
<LucideIcon.ArrowRight className="size-3.5 transition-transform group-hover:translate-x-0.5" />
</span>
</div>
</a>

<a
className="group flex flex-col overflow-hidden rounded-xl border bg-card transition-all hover:shadow-sm"
href="https://ghost.org/resources/first-100-email-subscribers/"
rel="noopener noreferrer"
target="_blank"
>
<div
className="h-36 w-full bg-cover bg-center"
style={{backgroundImage: `url(${assetRoot}img/marketing/members-2.jpg)`}}
/>
<div className="flex grow flex-col p-5">
<h4 className="text-sm font-semibold">
Get your first 100 email subscribers
</h4>
<p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">
Starting from zero? Use this guide to find your founding audience members.
</p>
<span className="mt-3 inline-flex items-center gap-1 text-sm font-medium text-foreground">
Become an expert
<LucideIcon.ArrowRight className="size-3.5 transition-transform group-hover:translate-x-0.5" />
</span>
</div>
</a>
</div>
</div>
</div>
);
};

export default MembersEmptyState;
36 changes: 18 additions & 18 deletions apps/posts/src/views/members/members.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import MembersActions from './components/members-actions';
import MembersContent from './components/members-content';
import MembersEmptyState from './components/members-empty-state';
import MembersFilters from './components/members-filters';
import MembersHeader from './components/members-header';
import MembersHeaderSearch from './components/members-header-search';
Expand Down Expand Up @@ -187,26 +188,25 @@
</Button>
</div>
) : !data?.members.length ? (
<div className="flex h-full flex-col items-center justify-center">
{hasFilterOrSearch ? (
<>
<EmptyIndicator title="No matching members found.">
<LucideIcon.Users />
</EmptyIndicator>
<Button
className="mt-4"
variant="outline"
onClick={() => clearAll({replace: false})}
>
Show all members
</Button>
</>
) : (
<EmptyIndicator title="No members yet">
hasFilterOrSearch ? (
<div className="flex h-full flex-col items-center justify-center">
<EmptyIndicator
actions={
<Button
variant="outline"
onClick={() => clearAll({replace: false})}
>
Show all members
</Button>
}
title="No matching members found."
>
<LucideIcon.Users />
</EmptyIndicator>
)}
</div>
</div>
) : (
<MembersEmptyState onMemberCreated={() => void refetch()} />

Check failure on line 208 in apps/posts/src/views/members/members.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of the "void" operator.

See more on https://sonarcloud.io/project/issues?id=TryGhost_Ghost&issues=AZ2qguYFCdC7ADWUds-e&open=AZ2qguYFCdC7ADWUds-e&pullRequest=27463
)

Check warning on line 209 in apps/posts/src/views/members/members.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=TryGhost_Ghost&issues=AZ2qguYFCdC7ADWUds-d&open=AZ2qguYFCdC7ADWUds-d&pullRequest=27463
) : (
<MembersList
activeColumns={activeColumns}
Expand Down
2 changes: 1 addition & 1 deletion apps/shade/src/components/ui/empty-indicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const EmptyIndicator = React.forwardRef<HTMLDivElement, EmptyIndicatorProps>(({c
}
</div>
{actions && (
<div className='mt-4 flex items-center gap-2'>
<div className='flex items-center gap-2'>
{actions}
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion e2e/helpers/pages/admin/members/members-list-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export class MembersListPage extends AdminPage implements MembersListSurface {
this.newMemberButton = page.getByRole('link', {name: 'New member'});
this.filterButton = page.getByRole('button', {name: /^(Filter|Add filter)$/});
this.clearFiltersButton = page.getByRole('button', {name: 'Clear'});
this.emptyState = page.getByText('No members yet');
this.emptyState = page.getByText('Start building your audience');
this.noResults = page.getByText('No matching members found.');
this.showAllButton = page.getByRole('button', {name: 'Show all members'});
}
Expand Down
Loading