diff --git a/__tests__/components/home/LatestDropNextMintSubscribe.test.tsx b/__tests__/components/home/LatestDropNextMintSubscribe.test.tsx
new file mode 100644
index 0000000000..2d0553e19c
--- /dev/null
+++ b/__tests__/components/home/LatestDropNextMintSubscribe.test.tsx
@@ -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 (
+
+ 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)}
+
+ );
+ }
+);
+
+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();
+
+ 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();
+
+ expect(screen.getByTestId("meme-subscription-row")).toHaveTextContent(
+ "eligibility:2"
+ );
+ });
+
+ it("does not render when there is no connected profile", () => {
+ const { container } = renderWithAuth(
+ ,
+ { connectedProfile: null }
+ );
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("does not render during an active proxy session", () => {
+ const { container } = renderWithAuth(
+ ,
+ {
+ activeProfileProxy: {
+ id: "proxy-1",
+ granted_to: {} as any,
+ created_at: Date.now(),
+ created_by: {} as any,
+ actions: [],
+ } as any,
+ }
+ );
+
+ expect(container).toBeEmptyDOMElement();
+ });
+});
diff --git a/components/home/now-minting/ArtistPill.tsx b/components/home/now-minting/ArtistPill.tsx
new file mode 100644
index 0000000000..d66899607b
--- /dev/null
+++ b/components/home/now-minting/ArtistPill.tsx
@@ -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 = (
+
+ {resolvedPfp ? (
+
+ ) : (
+
+ )}
+ {label}
+
+ );
+
+ if (!href) {
+ return content;
+ }
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/components/home/now-minting/LatestDropNextMintSection.tsx b/components/home/now-minting/LatestDropNextMintSection.tsx
index 102fdc5449..5d86258138 100644
--- a/components/home/now-minting/LatestDropNextMintSection.tsx
+++ b/components/home/now-minting/LatestDropNextMintSection.tsx
@@ -1,7 +1,6 @@
"use client";
import MediaTypeBadge from "@/components/drops/media/MediaTypeBadge";
-import { resolveIpfsUrl } from "@/components/ipfs/IPFSContext";
import {
getCanonicalNextMintNumber,
formatFullDateTime,
@@ -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 = (
-
- {pfp ? (
-
- ) : (
-
- )}
-
- {label}
-
-
- );
-
- if (!href) {
- return content;
- }
-
- return (
-
- {content}
-
- );
-}
-
const formatDropTimestamp = (timestamp: number): string | null => {
const date = new Date(timestamp);
if (Number.isNaN(date.getTime())) {
@@ -175,14 +134,18 @@ export default function LatestDropNextMintSection({
mimeType={media.mime_type}
dropId={drop.id}
size="sm"
+ iconClassName="tw-size-[26px]"
/>
)}
-
+
+
diff --git a/components/home/now-minting/LatestDropNextMintSubscribe.tsx b/components/home/now-minting/LatestDropNextMintSubscribe.tsx
new file mode 100644
index 0000000000..9551a2f1b3
--- /dev/null
+++ b/components/home/now-minting/LatestDropNextMintSubscribe.tsx
@@ -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;
+
+ const profileKey = useMemo(
+ () => (activeProfileProxy ? undefined : getProfileKey(connectedProfile)),
+ [activeProfileProxy, connectedProfile]
+ );
+
+ const { data: details } = useQuery
({
+ queryKey: ["next-mint-subscription-details", profileKey],
+ queryFn: async () =>
+ await commonApiFetch({
+ endpoint: `subscriptions/consolidation/details/${profileKey}`,
+ }),
+ enabled: !!profileKey,
+ });
+
+ const {
+ data: status,
+ refetch: refetchStatus,
+ } = useQuery({
+ queryKey: ["next-mint-subscription-status", profileKey, tokenId],
+ queryFn: async () =>
+ await commonApiFetch({
+ endpoint: `subscriptions/consolidation/upcoming-memes/${tokenId}/${profileKey}`,
+ }),
+ enabled: !!profileKey && hasTokenId,
+ });
+
+ const subscription = useMemo(() => {
+ 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 (
+
+
+ {
+ refetchStatus();
+ }}
+ minting_today={isMintingToday()}
+ first
+ date={null}
+ variant="compact"
+ />
+
+
+ );
+}
diff --git a/components/home/now-minting/NowMintingHeader.tsx b/components/home/now-minting/NowMintingHeader.tsx
index bd420b2a58..71f136f682 100644
--- a/components/home/now-minting/NowMintingHeader.tsx
+++ b/components/home/now-minting/NowMintingHeader.tsx
@@ -1,12 +1,8 @@
"use client";
import MediaTypeBadge from "@/components/drops/media/MediaTypeBadge";
-import { resolveIpfsUrl } from "@/components/ipfs/IPFSContext";
-import ArtistProfileHandle from "@/components/the-memes/ArtistProfileHandle";
-import type { BaseNFT } from "@/entities/INFT";
-import { useIdentity } from "@/hooks/useIdentity";
-import Image from "next/image";
import Link from "next/link";
+import ArtistPill from "./ArtistPill";
interface NowMintingHeaderProps {
readonly cardNumber: number;
@@ -16,53 +12,6 @@ interface NowMintingHeaderProps {
readonly mediaMimeType?: string | null | undefined;
}
-function NowMintingArtistHandlePill({
- artistHandle,
-}: {
- readonly artistHandle: string;
-}) {
- const { profile } = useIdentity({
- handleOrWallet: artistHandle,
- initialProfile: null,
- });
-
- return (
-
- {profile?.pfp ? (
-
- ) : (
-
- )}
-
-
-
-
- );
-}
-
-function NowMintingArtistNamePill({
- artistName,
-}: {
- readonly artistName: string;
-}) {
- return (
-
-
-
- {artistName}
-
-
- );
-}
-
export default function NowMintingHeader({
cardNumber,
title,
@@ -90,7 +39,7 @@ export default function NowMintingHeader({
mimeType={mediaMimeType}
dropId={`home-now-minting-${cardNumber}`}
size="sm"
- iconClassName="tw-size-[26px] tw-rounded-full"
+ iconClassName="tw-size-[26px]"
/>
)}
@@ -98,10 +47,15 @@ export default function NowMintingHeader({
{artistHandles.length > 0 ? (
artistHandles.map((handle) => (
-
+
))
) : (
-
+
)}
diff --git a/components/user/subscriptions/MemeSubscriptionRow.tsx b/components/user/subscriptions/MemeSubscriptionRow.tsx
new file mode 100644
index 0000000000..9b985dd7a7
--- /dev/null
+++ b/components/user/subscriptions/MemeSubscriptionRow.tsx
@@ -0,0 +1,398 @@
+"use client";
+
+import { AuthContext } from "@/components/auth/Auth";
+import { Spinner } from "@/components/dotLoader/DotLoader";
+import type { SeasonMintRow } from "@/components/meme-calendar/meme-calendar.helpers";
+import {
+ displayedSeasonNumberFromIndex,
+ formatFullDate,
+} from "@/components/meme-calendar/meme-calendar.helpers";
+import type { NFTFinalSubscription } from "@/generated/models/NFTFinalSubscription";
+import type { NFTSubscription } from "@/generated/models/NFTSubscription";
+import { formatAddress } from "@/helpers/Helpers";
+import { commonApiFetch, commonApiPost } from "@/services/api/common-api";
+import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { useContext, useEffect, useMemo, useState } from "react";
+import { Col, Container, Row } from "react-bootstrap";
+import Toggle from "react-toggle";
+import { Tooltip } from "react-tooltip";
+
+export default function MemeSubscriptionRow(
+ props: Readonly<{
+ profileKey: string;
+ title: string;
+ subscription: NFTSubscription;
+ eligibilityCount: number;
+ readonly: boolean;
+ minting_today?: boolean | undefined;
+ first: boolean;
+ date: SeasonMintRow | null;
+ refresh: () => void;
+ variant?: "default" | "compact";
+ }>
+) {
+ const id = `subscription-${props.subscription.token_id}`;
+ const isCompact = props.variant === "compact";
+
+ const queryClient = useQueryClient();
+ const { requestAuth, setToast } = useContext(AuthContext);
+
+ const [subscribed, setSubscribed] = useState(
+ !!props.subscription.subscribed
+ );
+
+ const subscribedCount = useMemo(
+ () => props.subscription.subscribed_count ?? 1,
+ [props.subscription.subscribed_count]
+ );
+
+ const [selectedCount, setSelectedCount] = useState(subscribedCount);
+ const countOptions = useMemo(
+ () => Array.from({ length: props.eligibilityCount }, (_, i) => i + 1),
+ [props.eligibilityCount]
+ );
+
+ useEffect(() => {
+ setSelectedCount(subscribedCount);
+ }, [subscribedCount]);
+
+ useEffect(() => {
+ if (selectedCount > props.eligibilityCount) {
+ setSelectedCount(Math.max(0, props.eligibilityCount));
+ }
+ }, [props.eligibilityCount, selectedCount]);
+
+ const { data: fetchedFinal } = useQuery({
+ queryKey: [
+ "consolidation-final-subscription",
+ `${props.profileKey}-${props.subscription.contract}-${props.subscription.token_id}`,
+ ],
+ queryFn: async () =>
+ await commonApiFetch({
+ endpoint: `subscriptions/consolidation/final/${props.profileKey}/${props.subscription.contract}/${props.subscription.token_id}`,
+ }),
+ enabled: props.first,
+ });
+ const final = fetchedFinal;
+
+ useEffect(() => {
+ setSubscribed(!!props.subscription.subscribed);
+ }, [props.subscription.subscribed]);
+
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const isToggleDisabled =
+ props.readonly ||
+ isSubmitting ||
+ props.minting_today ||
+ (!subscribed && props.eligibilityCount < 1);
+ const finalWithMetadata = useMemo(() => {
+ if (
+ !props.first ||
+ !final?.phase ||
+ final.phase_position === undefined ||
+ final.phase_position <= 0
+ ) {
+ return null;
+ }
+
+ return {
+ phase: final.phase,
+ phasePosition: final.phase_position,
+ phaseSubscriptions: final.phase_subscriptions ?? 0,
+ airdropAddress: final.airdrop_address,
+ subscribedCount: final.subscribed_count,
+ };
+ }, [final, props.first]);
+
+ const submit = async (): Promise => {
+ if (isSubmitting || props.minting_today) {
+ return;
+ }
+ interface SubscribeBody {
+ contract: string;
+ token_id: number;
+ subscribed: boolean;
+ }
+
+ setIsSubmitting(true);
+ try {
+ const { success } = await requestAuth();
+ if (!success) {
+ return;
+ }
+
+ const subscribe = !subscribed;
+ const response = await commonApiPost({
+ endpoint: `subscriptions/${props.profileKey}/subscription`,
+ body: {
+ contract: props.subscription.contract,
+ token_id: props.subscription.token_id,
+ subscribed: subscribe,
+ },
+ });
+ const responseSubscribed = response.subscribed;
+ setSubscribed(!!responseSubscribed);
+ const detail = responseSubscribed
+ ? "Subscribed for"
+ : "Unsubscribed from";
+ setToast({
+ message: `${detail} ${props.title} #${response.token_id}`,
+ type: "success",
+ });
+ props.refresh();
+ queryClient.invalidateQueries({
+ queryKey: [
+ "consolidation-final-subscription",
+ `${props.profileKey}-${props.subscription.contract}-${props.subscription.token_id}`,
+ ],
+ });
+ } catch (e: unknown) {
+ setToast({
+ message:
+ typeof e === "string" ? e : "Failed to change token subscription.",
+ type: "error",
+ });
+ return;
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleUpdateSubscriptionCount = async (
+ value: number
+ ): Promise => {
+ if (isSubmitting || props.minting_today) {
+ return;
+ }
+ interface UpdateSubscriptionCountBody {
+ contract: string;
+ token_id: number;
+ count: number;
+ }
+
+ setIsSubmitting(true);
+ try {
+ const { success } = await requestAuth();
+ if (!success) {
+ setSelectedCount(subscribedCount);
+ return;
+ }
+
+ const response = await commonApiPost<
+ UpdateSubscriptionCountBody,
+ UpdateSubscriptionCountBody
+ >({
+ endpoint: `subscriptions/${props.profileKey}/subscription-count`,
+ body: {
+ contract: props.subscription.contract,
+ token_id: props.subscription.token_id,
+ count: value,
+ },
+ });
+ const responseCount = response.count;
+ setSelectedCount(responseCount);
+ setToast({
+ message: `Subscription count updated to ${responseCount} for ${props.title} #${props.subscription.token_id}`,
+ type: "success",
+ });
+ props.refresh();
+ queryClient.invalidateQueries({
+ queryKey: [
+ "consolidation-final-subscription",
+ `${props.profileKey}-${props.subscription.contract}-${props.subscription.token_id}`,
+ ],
+ });
+ } catch (e: unknown) {
+ setSelectedCount(subscribedCount);
+ setToast({
+ message:
+ typeof e === "string" ? e : "Failed to update subscription count.",
+ type: "error",
+ });
+ return;
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleCountChange = async (value: string): Promise => {
+ const nextValue = Number.parseInt(value, 10);
+ setSelectedCount(nextValue);
+ await handleUpdateSubscriptionCount(nextValue);
+ };
+
+ const renderCountSelector = ({
+ selectClassName,
+ disableWhenSingleOption,
+ }: {
+ selectClassName: string;
+ disableWhenSingleOption: boolean;
+ }) => {
+ if (!subscribed) {
+ return null;
+ }
+
+ return (
+ <>
+
+ / {props.eligibilityCount}
+ >
+ );
+ };
+
+ if (isCompact) {
+ return (
+
+
+
Subscribe
+
+ {isSubmitting && }
+
+
+ {renderCountSelector({
+ selectClassName:
+ "tw-rounded tw-border tw-border-iron-400 tw-bg-transparent tw-px-1 tw-text-iron-400",
+ disableWhenSingleOption: false,
+ })}
+
+
+
+ {finalWithMetadata && (
+
+ Phase: {finalWithMetadata.phase} - Subscription Position:{" "}
+ {finalWithMetadata.phasePosition.toLocaleString()} /{" "}
+ {finalWithMetadata.phaseSubscriptions.toLocaleString()} - Airdrop
+ Address: {formatAddress(finalWithMetadata.airdropAddress)} -
+ Subscription Count: x{finalWithMetadata.subscribedCount}
+
+ )}
+ {props.minting_today && (
+
+
+ Minting Today{" "}
+
+
+
+ No changes allowed on minting day
+
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {props.title} #{props.subscription.token_id}{" "}
+
+ {props.date && (
+ <>
+
+ - SZN
+ {displayedSeasonNumberFromIndex(props.date.seasonIndex)}
+
+ {" / "}
+ {props.minting_today ? (
+ <>
+
+ - Minting Today{" "}
+
+
+
+ No changes allowed on minting day
+
+ >
+ ) : (
+ {formatFullDate(props.date.utcDay)}
+ )}
+ >
+ )}
+
+ {finalWithMetadata && (
+
+ Phase: {finalWithMetadata.phase} - Subscription Position:{" "}
+ {finalWithMetadata.phasePosition.toLocaleString()} /{" "}
+ {finalWithMetadata.phaseSubscriptions.toLocaleString()} -
+ Airdrop Address:{" "}
+ {formatAddress(finalWithMetadata.airdropAddress)} -
+ Subscription Count: x{finalWithMetadata.subscribedCount}
+
+ )}
+
+
+ {isSubmitting && }
+
+
+ {renderCountSelector({
+ selectClassName:
+ "tw-text-iron-400 tw-bg-transparent tw-border tw-border-iron-400 tw-rounded tw-px-1",
+ disableWhenSingleOption: true,
+ })}
+
+
+
+
+
+ );
+}
diff --git a/components/user/subscriptions/UserPageSubscriptionsUpcoming.tsx b/components/user/subscriptions/UserPageSubscriptionsUpcoming.tsx
index f800f8c953..94d9153d75 100644
--- a/components/user/subscriptions/UserPageSubscriptionsUpcoming.tsx
+++ b/components/user/subscriptions/UserPageSubscriptionsUpcoming.tsx
@@ -1,28 +1,16 @@
"use client";
-
-import { AuthContext } from "@/components/auth/Auth";
-import { Spinner } from "@/components/dotLoader/DotLoader";
import type {
SeasonMintRow} from "@/components/meme-calendar/meme-calendar.helpers";
import {
- displayedSeasonNumberFromIndex,
- formatFullDate,
getUpcomingMintsAcrossSeasons,
isMintingToday
} from "@/components/meme-calendar/meme-calendar.helpers";
import ShowMoreButton from "@/components/show-more-button/ShowMoreButton";
-import type { NFTFinalSubscription } from "@/generated/models/NFTFinalSubscription";
import type { NFTSubscription } from "@/generated/models/NFTSubscription";
import type { SubscriptionDetails } from "@/generated/models/SubscriptionDetails";
-import { formatAddress } from "@/helpers/Helpers";
-import { commonApiFetch, commonApiPost } from "@/services/api/common-api";
-import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useContext, useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { Col, Container, Row } from "react-bootstrap";
-import Toggle from "react-toggle";
-import { Tooltip } from "react-tooltip";
+import MemeSubscriptionRow from "./MemeSubscriptionRow";
import styles from "./UserPageSubscriptions.module.scss";
export default function UserPageSubscriptionsUpcoming(
@@ -74,7 +62,7 @@ export default function UserPageSubscriptionsUpcoming(
className={`${styles["nftSubscriptionsListItem"]} ${
index % 2 === 0 ? styles["odd"] : styles["even"]
} ${index === subscriptions.length - 1 ? styles["last"] : ""}`}>
-
);
}
-
-function SubscriptionRow(
- props: Readonly<{
- profileKey: string;
- title: string;
- subscription: NFTSubscription;
- eligibilityCount: number;
- readonly: boolean;
- minting_today?: boolean | undefined;
- first: boolean;
- date: SeasonMintRow | null;
- refresh: () => void;
- }>
-) {
- const id = `subscription-${props.subscription.token_id}`;
-
- const queryClient = useQueryClient();
- const { requestAuth, setToast } = useContext(AuthContext);
-
- const [subscribed, setSubscribed] = useState(
- !!props.subscription.subscribed
- );
-
- const subscribedCount = useMemo(
- () => props.subscription.subscribed_count ?? 1,
- [props.subscription.subscribed_count]
- );
-
- const [selectedCount, setSelectedCount] = useState(subscribedCount);
-
- useEffect(() => {
- setSelectedCount(subscribedCount);
- }, [subscribedCount]);
-
- useEffect(() => {
- if (selectedCount > props.eligibilityCount) {
- setSelectedCount(Math.max(1, props.eligibilityCount));
- }
- }, [props.eligibilityCount, selectedCount]);
-
- const { data: final } = useQuery({
- queryKey: [
- "consolidation-final-subscription",
- `${props.profileKey}-${props.subscription.contract}-${props.subscription.token_id}`,
- ],
- queryFn: async () =>
- await commonApiFetch({
- endpoint: `subscriptions/consolidation/final/${props.profileKey}/${props.subscription.contract}/${props.subscription.token_id}`,
- }),
- enabled: props.first,
- });
-
- useEffect(() => {
- setSubscribed(!!props.subscription.subscribed);
- }, [props.subscription.subscribed]);
-
- const [isSubmitting, setIsSubmitting] = useState(false);
-
- const submit = async (): Promise => {
- if (isSubmitting || props.minting_today) {
- return;
- }
- setIsSubmitting(true);
- const { success } = await requestAuth();
- if (!success) {
- setIsSubmitting(false);
- return;
- }
- const subscribe = !subscribed;
- interface SubscribeBody {
- contract: string;
- token_id: number;
- subscribed: boolean;
- }
- try {
- const response = await commonApiPost({
- endpoint: `subscriptions/${props.profileKey}/subscription`,
- body: {
- contract: props.subscription.contract,
- token_id: props.subscription.token_id,
- subscribed: subscribe,
- },
- });
- const responseSubscribed = response.subscribed;
- setSubscribed(!!responseSubscribed);
- const detail = responseSubscribed
- ? `Subscribed for`
- : `Unsubscribed from`;
- setToast({
- message: `${detail} ${props.title} #${response.token_id}`,
- type: "success",
- });
- props.refresh();
- queryClient.invalidateQueries({
- queryKey: [
- "consolidation-final-subscription",
- `${props.profileKey}-${props.subscription.contract}-${props.subscription.token_id}`,
- ],
- });
- } catch (e: unknown) {
- setToast({
- message:
- typeof e === "string" ? e : "Failed to change token subscription.",
- type: "error",
- });
- return;
- } finally {
- setIsSubmitting(false);
- }
- };
-
- const handleUpdateSubscriptionCount = async (
- value: number
- ): Promise => {
- if (isSubmitting || props.minting_today) {
- return;
- }
- setIsSubmitting(true);
- const { success } = await requestAuth();
- if (!success) {
- setIsSubmitting(false);
- return;
- }
- interface UpdateSubscriptionCountBody {
- contract: string;
- token_id: number;
- count: number;
- }
- try {
- const response = await commonApiPost<
- UpdateSubscriptionCountBody,
- UpdateSubscriptionCountBody
- >({
- endpoint: `subscriptions/${props.profileKey}/subscription-count`,
- body: {
- contract: props.subscription.contract,
- token_id: props.subscription.token_id,
- count: value,
- },
- });
- const responseCount = response.count;
- setSelectedCount(responseCount);
- setToast({
- message: `Subscription count updated to ${responseCount} for ${props.title} #${props.subscription.token_id}`,
- type: "success",
- });
- props.refresh();
- queryClient.invalidateQueries({
- queryKey: [
- "consolidation-final-subscription",
- `${props.profileKey}-${props.subscription.contract}-${props.subscription.token_id}`,
- ],
- });
- } catch (e: unknown) {
- setSelectedCount(subscribedCount);
- setToast({
- message:
- typeof e === "string" ? e : "Failed to update subscription count.",
- type: "error",
- });
- return;
- } finally {
- setIsSubmitting(false);
- }
- };
-
- return (
-
-
-
-
-
-
- {props.title} #{props.subscription.token_id}{" "}
-
- {props.date && (
- <>
-
- - SZN
- {displayedSeasonNumberFromIndex(props.date.seasonIndex)}
-
- {" / "}
- {props.minting_today ? (
- <>
-
- - Minting Today{" "}
-
-
-
- No changes allowed on minting day
-
- >
- ) : (
- {formatFullDate(props.date.utcDay)}
- )}
- >
- )}
-
- {props.first &&
- final?.phase &&
- final.phase_position !== undefined &&
- final.phase_position > 0 && (
-
- Phase: {final.phase} - Subscription Position:{" "}
- {final.phase_position.toLocaleString()} /{" "}
- {(final.phase_subscriptions ?? 0).toLocaleString()} - Airdrop
- Address: {formatAddress(final.airdrop_address)} - Subscription
- Count: x{final.subscribed_count}
-
- )}
-
-
- {isSubmitting && }
-
-
- {subscribed ? (
- <>
-
-
- / {props.eligibilityCount}
-
- >
- ) : null}
-
-
-
-
-
- );
-}