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
114 changes: 60 additions & 54 deletions apps/posts/src/views/members/components/members-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ interface MembersActionsProps {
nql?: string;
search: string;
canBulkDelete: boolean;
showMenu?: boolean;
showNewMember?: boolean;
onImportComplete?: (importResponse?: ImportResponse) => void;
}

Expand All @@ -37,6 +39,8 @@ const MembersActions: React.FC<MembersActionsProps> = ({
nql,
search,
canBulkDelete,
showMenu = true,
showNewMember = true,
onImportComplete
}) => {
const location = useLocation();
Expand Down Expand Up @@ -209,67 +213,69 @@ const MembersActions: React.FC<MembersActionsProps> = ({

return (
<>
{/* Actions Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button data-testid="members-actions" variant="outline">
<LucideIcon.MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/* Import */}
<DropdownMenuItem onClick={handleImportAction}>
<LucideIcon.Upload className="mr-2 size-4" />
Import members
</DropdownMenuItem>
{showMenu && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button data-testid="members-actions" variant="outline">
<LucideIcon.MoreHorizontal className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{/* Import */}
<DropdownMenuItem onClick={handleImportAction}>
<LucideIcon.Upload className="mr-2 size-4" />
Import members
</DropdownMenuItem>

{memberCount > 0 && (
<>
{/* Export */}
<DropdownMenuItem onClick={handleExport}>
<LucideIcon.Download className="mr-2 size-4" />
{hasFilterOrSearch
? `Export ${formatNumber(memberCount)} members`
: 'Export all members'}
</DropdownMenuItem>
{memberCount > 0 && (
<>
{/* Export */}
<DropdownMenuItem onClick={handleExport}>
<LucideIcon.Download className="mr-2 size-4" />
{hasFilterOrSearch
? `Export ${formatNumber(memberCount)} members`
: 'Export all members'}
</DropdownMenuItem>

<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setShowAddLabelModal(true)}>
<LucideIcon.Tags className="mr-2 size-4" />
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setShowAddLabelModal(true)}>
<LucideIcon.Tags className="mr-2 size-4" />
Add label to {formatNumber(memberCount)} {memberCount === 1 ? 'member' : 'members'}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setShowRemoveLabelModal(true)}>
<LucideIcon.Tag className="mr-2 size-4" />
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setShowRemoveLabelModal(true)}>
<LucideIcon.Tag className="mr-2 size-4" />
Remove label from {formatNumber(memberCount)} {memberCount === 1 ? 'member' : 'members'}
</DropdownMenuItem>
<DropdownMenuItem
disabled={isLoadingNewsletters}
onClick={() => setShowUnsubscribeModal(true)}
>
<LucideIcon.MailX className="mr-2 size-4" />
</DropdownMenuItem>
<DropdownMenuItem
disabled={isLoadingNewsletters}
onClick={() => setShowUnsubscribeModal(true)}
>
<LucideIcon.MailX className="mr-2 size-4" />
Unsubscribe {formatNumber(memberCount)} {memberCount === 1 ? 'member' : 'members'}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
disabled={!canBulkDelete}
onClick={() => setShowDeleteModal(true)}
>
<LucideIcon.Trash2 className="mr-2 size-4" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive focus:text-destructive"
disabled={!canBulkDelete}
onClick={() => setShowDeleteModal(true)}
>
<LucideIcon.Trash2 className="mr-2 size-4" />
Delete {formatNumber(memberCount)} {memberCount === 1 ? 'member' : 'members'}
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
)}

{/* New Member Button - styled like Tags */}
<Button asChild>
<a aria-label="New member" className="inline-flex items-center" href={newMemberHref}>
<span className="hidden sm:inline">New member</span>
<span className="sm:hidden"><LucideIcon.Plus /></span>
</a>
</Button>
{showNewMember && (
<Button asChild>
<a aria-label="New member" className="inline-flex items-center" href={newMemberHref}>
<span className="hidden sm:inline">New member</span>
<span className="sm:hidden"><LucideIcon.Plus /></span>
</a>
</Button>
)}

{/* Modals */}
<ImportMembersModal
Expand Down
45 changes: 19 additions & 26 deletions apps/posts/src/views/members/components/members-empty-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {toast} from 'sonner';
import {useAddMember} from '@tryghost/admin-x-framework/api/members';
import {useCurrentUser} from '@tryghost/admin-x-framework/api/current-user';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useNavigate} from '@tryghost/admin-x-framework';

interface MembersEmptyStateProps {
membershipsEnabled: boolean;
Expand All @@ -16,7 +15,6 @@ const MembersEmptyState: React.FC<MembersEmptyStateProps> = ({membershipsEnabled
const {data: currentUser, isLoading: isCurrentUserLoading} = useCurrentUser();
const {mutateAsync: addMember, isLoading: isAdding} = useAddMember();
const handleError = useHandleError();
const navigate = useNavigate();

const handleAddYourself = useCallback(async () => {
if (!currentUser || isAdding) {
Expand Down Expand Up @@ -55,36 +53,31 @@ const MembersEmptyState: React.FC<MembersEmptyStateProps> = ({membershipsEnabled

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">
<EmptyIndicator
actions={
<div className="flex flex-col items-center gap-3">
<div className="flex flex-col items-center gap-2 sm:flex-row">
<Button asChild>
<a href="#/members/new?back=%2Fmembers">New member</a>
</Button>
<Button
disabled={isAdding || isCurrentUserLoading || !currentUser}
variant="outline"
onClick={handleAddYourself}
>
{isAdding ? 'Adding...' : 'Add yourself as a member to test'}
{isAdding ? 'Adding...' : 'Add yourself as a member'}
</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>
<p className="text-sm leading-tight text-pretty text-muted-foreground">
Already have members? <a className="font-medium text-foreground underline-offset-4 hover:underline" href="#/members/import">Import with CSV</a>
</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>
);
};
Expand Down
94 changes: 48 additions & 46 deletions apps/posts/src/views/members/components/members-help-cards.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,60 @@
import React from 'react';
import membersAudienceImage from '../../../assets/members/members-1.jpg';
import membersSubscribersImage from '../../../assets/members/members-2.jpg';
import {LucideIcon} from '@tryghost/shade/utils';
import {LucideIcon, cn} from '@tryghost/shade/utils';

const MembersHelpCards: React.FC = () => {
interface MembersHelpCardProps {
children: React.ReactNode;
description: string;
title: string;
url: string;
}

const MembersHelpCard: React.FC<MembersHelpCardProps> = ({children, description, title, url}) => {
return (
<div className="mx-auto grid w-full max-w-lg 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(${membersAudienceImage})`}}
/>
<div className="flex grow flex-col p-5">
<h4 className="text-sm font-semibold">
Building your audience with subscriber signups
<a
className="group/card block rounded-xl border bg-card p-6 transition-all hover:bg-accent/50 hover:shadow-xs"
href={url}
rel="noopener noreferrer"
target="_blank"
>
<div className="flex items-center gap-6">
{children}
<div className="flex flex-col leading-tight">
<h4 className="text-md font-medium tracking-normal text-pretty text-foreground">
{title}
</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 className="mt-1.5 text-sm leading-tight text-pretty text-muted-foreground">
{description}
</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>
</div>
</a>
);
};

const iconCardClass = 'flex h-18 w-[100px] min-w-[100px] items-center justify-center rounded-md p-4 opacity-80 transition-all group-hover/card:opacity-100';

<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"
const MembersHelpCards: React.FC = () => {
return (
<div className="grid w-full grid-cols-1 gap-4 lg:grid-cols-2">
<MembersHelpCard
description="Learn how to turn anonymous visitors into logged-in members with memberships in Ghost."
title="Building your audience with subscriber signups"
url="https://ghost.org/resources/build-audience-subscriber-signups/"
>
<div
className="h-36 w-full bg-cover bg-center"
style={{backgroundImage: `url(${membersSubscribersImage})`}}
/>
<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 className={cn(iconCardClass, 'bg-gradient-to-tr from-[#22C55E]/20 to-[#84CC16]/20')}>
<LucideIcon.UserPlus className="text-[#16A34A]" size={20} strokeWidth={1.5} />
</div>
</MembersHelpCard>

<MembersHelpCard
description="Starting from zero? Use this guide to find your founding audience members."
title="Get your first 100 email subscribers"
url="https://ghost.org/resources/first-100-email-subscribers/"
>
<div className={cn(iconCardClass, 'bg-gradient-to-tl from-[#A855F7]/20 to-[#EC4899]/20')}>
<LucideIcon.Mail className="text-[#DB2777]" size={20} strokeWidth={1.5} />
</div>
</a>
</MembersHelpCard>
</div>
);
};
Expand Down
Loading
Loading