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
152 changes: 152 additions & 0 deletions __tests__/components/home/LatestDropNextMintSubscribe.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { renderWithAuth } from "@/__tests__/utils/testContexts";
import LatestDropNextMintSubscribe from "@/components/home/now-minting/LatestDropNextMintSubscribe";
import { useQuery } from "@tanstack/react-query";
import { screen } from "@testing-library/react";

jest.mock("@tanstack/react-query", () => ({
useQuery: jest.fn(),
}));

jest.mock(
"@/components/user/subscriptions/MemeSubscriptionRow",
() =>
function MockMemeSubscriptionRow(props: any) {
return (
<div data-testid="meme-subscription-row">
token:{props.subscription.token_id} eligibility:{props.eligibilityCount}
minting_today:{String(props.minting_today)} readonly:
{String(props.readonly)} variant:{props.variant ?? "default"} date:
{String(props.date)}
</div>
);
}
);

jest.mock("@/components/meme-calendar/meme-calendar.helpers", () => ({
__esModule: true,
getCanonicalNextMintNumber: jest.fn(() => 478),
getUpcomingMintsAcrossSeasons: jest.fn(() => [
{
utcDay: new Date("2026-04-03T00:00:00Z"),
instantUtc: new Date("2026-04-03T15:40:00Z"),
meme: 478,
seasonIndex: 15,
},
]),
isMintingToday: jest.fn(() => false),
}));

const useQueryMock = useQuery as jest.Mock;

describe("LatestDropNextMintSubscribe", () => {
beforeEach(() => {
useQueryMock.mockImplementation(({ queryKey }) => {
if (queryKey[0] === "next-mint-subscription-details") {
return {
data: {
subscription_eligibility_count: 3,
},
};
}

if (queryKey[0] === "next-mint-subscription-status") {
return {
data: {
subscribed: true,
eligibility: 2,
count: 2,
},
refetch: jest.fn(),
};
}

return {
data: null,
refetch: jest.fn(),
};
});
});

afterEach(() => {
jest.clearAllMocks();
});

it("renders the subscribe section for the connected profile", () => {
renderWithAuth(<LatestDropNextMintSubscribe />);

expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent(
/token:478/
);
expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent(
/eligibility:3/
);
expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent(
/minting_today:false/
);
expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent(
/readonly:false/
);
expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent(
/variant:compact/
);
expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent(
/date:null/
);
});

it("falls back to status eligibility when details are unavailable", () => {
useQueryMock.mockImplementation(({ queryKey }) => {
if (queryKey[0] === "next-mint-subscription-details") {
return { data: undefined };
}

if (queryKey[0] === "next-mint-subscription-status") {
return {
data: {
subscribed: true,
eligibility: 2,
count: 1,
},
refetch: jest.fn(),
};
}

return {
data: null,
refetch: jest.fn(),
};
});

renderWithAuth(<LatestDropNextMintSubscribe />);

expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent(
"eligibility:2"
);
});

it("does not render when there is no connected profile", () => {
const { container } = renderWithAuth(
<LatestDropNextMintSubscribe />,
{ connectedProfile: null }
);

expect(container).toBeEmptyDOMElement();
});

it("does not render during an active proxy session", () => {
const { container } = renderWithAuth(
<LatestDropNextMintSubscribe />,
{
activeProfileProxy: {
id: "proxy-1",
granted_to: {} as any,
created_at: Date.now(),
created_by: {} as any,
actions: [],
} as any,
}
);

expect(container).toBeEmptyDOMElement();
});
});
60 changes: 60 additions & 0 deletions components/home/now-minting/ArtistPill.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"use client";

import { resolveIpfsUrl } from "@/components/ipfs/IPFSContext";
import { useIdentity } from "@/hooks/useIdentity";
import Image from "next/image";
import Link from "next/link";

interface ArtistPillProps {
readonly label: string;
readonly href?: string | undefined;
readonly pfp?: string | null | undefined;
readonly profileHandle?: string | undefined;
}

export default function ArtistPill({
label,
href,
pfp,
profileHandle,
}: ArtistPillProps) {
const { profile } = useIdentity({
handleOrWallet: profileHandle ?? "",
initialProfile: null,
});

const resolvedPfp = pfp ?? profile?.pfp ?? null;
const labelClassName = href
? "tw-min-w-0 tw-truncate tw-text-sm tw-font-medium tw-text-iron-200 tw-transition-colors tw-duration-300 desktop-hover:hover:tw-text-iron-100"
: "tw-min-w-0 tw-truncate tw-text-sm tw-font-medium tw-text-iron-200";

const content = (
<span className="tw-inline-flex tw-min-w-0 tw-max-w-full tw-items-center tw-gap-2 tw-rounded-full tw-border tw-border-solid tw-border-white/10 tw-bg-white/5 tw-px-2.5 tw-py-1 tw-backdrop-blur-sm">
{resolvedPfp ? (
<Image
src={resolveIpfsUrl(resolvedPfp)}
alt={label}
width={16}
height={16}
className="tw-size-4 tw-flex-shrink-0 tw-rounded-sm tw-bg-iron-900 tw-object-contain"
/>
) : (
<span
aria-hidden="true"
className="tw-size-4 tw-flex-shrink-0 tw-rounded-sm tw-bg-iron-900"
/>
)}
<span className={labelClassName}>{label}</span>
</span>
);

if (!href) {
return content;
}

return (
<Link href={href} className="tw-no-underline">
{content}
</Link>
);
}
51 changes: 7 additions & 44 deletions components/home/now-minting/LatestDropNextMintSection.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import MediaTypeBadge from "@/components/drops/media/MediaTypeBadge";
import { resolveIpfsUrl } from "@/components/ipfs/IPFSContext";
import {
getCanonicalNextMintNumber,
formatFullDateTime,
Expand All @@ -15,54 +14,14 @@ import { getDropPreviewImageUrl } from "@/helpers/waves/drop.helpers";
import useDeviceInfo from "@/hooks/useDeviceInfo";
import Image from "next/image";
import Link from "next/link";
import ArtistPill from "./ArtistPill";
import LatestDropNextMintSubscribe from "./LatestDropNextMintSubscribe";
import NowMintingStatsItem from "./NowMintingStatsItem";

interface LatestDropNextMintSectionProps {
readonly drop: ApiDrop;
}

function NextMintArtistPill({
pfp,
label,
href,
}: {
readonly pfp: string | null | undefined;
readonly label: string;
readonly href?: string | undefined;
}) {
const content = (
<span className="tw-inline-flex tw-min-w-0 tw-max-w-full tw-items-center tw-gap-2 tw-rounded-full tw-border tw-border-solid tw-border-white/10 tw-bg-white/5 tw-px-2.5 tw-py-1 tw-backdrop-blur-sm">
{pfp ? (
<Image
src={resolveIpfsUrl(pfp)}
alt={label}
width={16}
height={16}
className="tw-size-4 tw-flex-shrink-0 tw-rounded-sm tw-bg-iron-900 tw-object-contain"
/>
) : (
<span
aria-hidden="true"
className="tw-size-4 tw-flex-shrink-0 tw-rounded-sm tw-bg-iron-900"
/>
)}
<span className="tw-min-w-0 tw-truncate tw-text-sm tw-font-medium tw-text-iron-200 desktop-hover:hover:tw-text-iron-100">
{label}
</span>
</span>
);

if (!href) {
return content;
}

return (
<Link href={href} className="tw-no-underline">
{content}
</Link>
);
}

const formatDropTimestamp = (timestamp: number): string | null => {
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) {
Expand Down Expand Up @@ -175,14 +134,18 @@ export default function LatestDropNextMintSection({
mimeType={media.mime_type}
dropId={drop.id}
size="sm"
iconClassName="tw-size-[26px]"
/>
)}
<NextMintArtistPill
<ArtistPill
pfp={author.pfp}
label={authorName}
href={authorHandle ? `/${authorHandle}` : undefined}
profileHandle={author.handle ?? undefined}
/>
</div>

<LatestDropNextMintSubscribe />
</div>

<div className="tw-grid tw-grid-cols-2 tw-gap-x-6 tw-gap-y-4 tw-border-x-0 tw-border-b-0 tw-border-t tw-border-solid tw-border-white/5 tw-pt-4">
Expand Down
99 changes: 99 additions & 0 deletions components/home/now-minting/LatestDropNextMintSubscribe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"use client";

import { AuthContext } from "@/components/auth/Auth";
import {
getCanonicalNextMintNumber,
isMintingToday,
} from "@/components/meme-calendar/meme-calendar.helpers";
import { MEMES_CONTRACT } from "@/constants/constants";
import type { ApiIdentity } from "@/generated/models/ApiIdentity";
import type { ApiUpcomingMemeSubscriptionStatus } from "@/generated/models/ApiUpcomingMemeSubscriptionStatus";
import type { NFTSubscription } from "@/generated/models/NFTSubscription";
import type { SubscriptionDetails } from "@/generated/models/SubscriptionDetails";
import { commonApiFetch } from "@/services/api/common-api";
import { useQuery } from "@tanstack/react-query";
import { useContext, useMemo } from "react";
import MemeSubscriptionRow from "../../user/subscriptions/MemeSubscriptionRow";

function getProfileKey(
connectedProfile: ApiIdentity | null
): string | undefined {
return (
connectedProfile?.consolidation_key ??
connectedProfile?.wallets?.map((wallet) => wallet.wallet).join("-")
);
}

export default function LatestDropNextMintSubscribe() {
const { connectedProfile, activeProfileProxy } = useContext(AuthContext);

const tokenId = useMemo(() => getCanonicalNextMintNumber(), []);
const hasTokenId = Number.isInteger(tokenId) && tokenId > 0;
Comment thread
prxt6529 marked this conversation as resolved.

const profileKey = useMemo(
() => (activeProfileProxy ? undefined : getProfileKey(connectedProfile)),
[activeProfileProxy, connectedProfile]
);

const { data: details } = useQuery<SubscriptionDetails>({
queryKey: ["next-mint-subscription-details", profileKey],
queryFn: async () =>
await commonApiFetch<SubscriptionDetails>({
endpoint: `subscriptions/consolidation/details/${profileKey}`,
}),
enabled: !!profileKey,
});

const {
data: status,
refetch: refetchStatus,
} = useQuery<ApiUpcomingMemeSubscriptionStatus>({
queryKey: ["next-mint-subscription-status", profileKey, tokenId],
queryFn: async () =>
await commonApiFetch<ApiUpcomingMemeSubscriptionStatus>({
endpoint: `subscriptions/consolidation/upcoming-memes/${tokenId}/${profileKey}`,
}),
enabled: !!profileKey && hasTokenId,
});

const subscription = useMemo<NFTSubscription | null>(() => {
if (!profileKey || !hasTokenId || !status) {
return null;
}

return {
consolidation_key: profileKey,
contract: MEMES_CONTRACT,
token_id: tokenId,
subscribed: status.subscribed,
subscribed_count: status.count ?? 1,
} as NFTSubscription;
}, [hasTokenId, profileKey, status, tokenId]);

if (!profileKey || !subscription) {
return null;
}

return (
<div className="tw-mt-4 tw-border-x-0 tw-border-b-0 tw-border-t tw-border-solid tw-border-white/5 tw-pt-4">
<div className="tw-rounded-xl tw-bg-transparent">
<MemeSubscriptionRow
profileKey={profileKey}
title="The Memes"
subscription={subscription}
eligibilityCount={
details?.subscription_eligibility_count ?? status?.eligibility ?? 1
}
readonly={false}
refresh={() => {
refetchStatus();
}}
minting_today={isMintingToday()}
first
date={null}
variant="compact"
/>
</div>
</div>
);
}
Loading
Loading