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 ? ( - {artistHandle} - ) : ( -
- )} - - - - - ); -} - -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} - -
- -
-
- ); -}