diff --git a/components/home/next-mint-leading/NextMintLeadingSection.tsx b/components/home/next-mint-leading/NextMintLeadingSection.tsx
index adbb6ad3aa..ac6dcb03a7 100644
--- a/components/home/next-mint-leading/NextMintLeadingSection.tsx
+++ b/components/home/next-mint-leading/NextMintLeadingSection.tsx
@@ -1,12 +1,13 @@
"use client";
-import { useSeizeSettings } from "@/contexts/SeizeSettingsContext";
-import { useNowMinting } from "@/hooks/useNowMinting";
+import { useNextMintDrop } from "@/hooks/useNextMintDrop";
+import { useNowMintingStatus } from "@/hooks/useNowMintingStatus";
import {
useWaveDropsLeaderboard,
WaveDropsLeaderboardSort,
} from "@/hooks/useWaveDropsLeaderboard";
-import { useWaveDecisions } from "@/hooks/waves/useWaveDecisions";
+import { ManifoldClaimStatus } from "@/hooks/useManifoldClaim";
+import { shouldShowNextWinnerInComingUp } from "@/helpers/mint-visibility.helpers";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import { LeadingCard } from "./LeadingCard";
@@ -19,15 +20,18 @@ const SKELETON_KEYS = [
];
export function NextMintLeadingSection() {
- const { seizeSettings, isLoaded } = useSeizeSettings();
- const { nft: nowMinting, isFetching: isNowMintingFetching } = useNowMinting();
-
- const waveId = seizeSettings.memes_wave_id;
-
- const { decisionPoints, isFetching: isWinnersFetching } = useWaveDecisions({
- waveId: waveId ?? "",
- enabled: !!waveId,
- });
+ const {
+ nft: nowMinting,
+ isFetching: isNowMintingFetching,
+ status: nowMintingStatus,
+ } = useNowMintingStatus();
+ const {
+ nextMint,
+ nextMintTitle,
+ waveId,
+ isReady,
+ isFetching: isWinnersFetching,
+ } = useNextMintDrop();
const { drops, isFetching: isLeaderboardFetching } = useWaveDropsLeaderboard({
waveId: waveId ?? "",
@@ -35,15 +39,6 @@ export function NextMintLeadingSection() {
pausePolling: !waveId,
});
- // Get latest winner (last decision, first place)
- const latestDecision = decisionPoints[decisionPoints.length - 1];
- const nextMint = latestDecision?.winners[0]?.drop ?? null;
-
- // Get nextMint title
- const nextMintTitle =
- nextMint?.title ??
- nextMint?.metadata.find((m) => m.data_key === "title")?.data_value;
-
// Compare with nowMinting name (case-insensitive, trimmed)
// Only treat as same when both values exist; otherwise treat as not equal
const isNextMintSameAsNowMinting =
@@ -52,7 +47,12 @@ export function NextMintLeadingSection() {
nowMinting.name.toLowerCase().trim() === nextMintTitle.toLowerCase().trim();
// Determine what to show
- const showNextMint = nextMint && !isNextMintSameAsNowMinting;
+ const canShowNextMint = shouldShowNextWinnerInComingUp({
+ isMintEnded: nowMintingStatus === ManifoldClaimStatus.ENDED,
+ nextMintExists: !!nextMint,
+ });
+ const showNextMint =
+ canShowNextMint && !!nextMint && !isNextMintSameAsNowMinting;
const leadingCount = showNextMint ? 2 : 3;
// Get top drops from leaderboard
@@ -61,7 +61,7 @@ export function NextMintLeadingSection() {
const isFetching =
isNowMintingFetching || isWinnersFetching || isLeaderboardFetching;
- if (!isLoaded || !waveId) {
+ if (!isReady || !waveId) {
return null;
}
diff --git a/components/home/now-minting/LatestDropNextMintSection.tsx b/components/home/now-minting/LatestDropNextMintSection.tsx
new file mode 100644
index 0000000000..8cbef56715
--- /dev/null
+++ b/components/home/now-minting/LatestDropNextMintSection.tsx
@@ -0,0 +1,189 @@
+"use client";
+
+import ProfileAvatar, {
+ ProfileBadgeSize,
+} from "@/components/common/profile/ProfileAvatar";
+import {
+ formatFullDateTime,
+ getNextMintStart,
+} from "@/components/meme-calendar/meme-calendar.helpers";
+import DropListItemContentMedia from "@/components/drops/view/item/content/media/DropListItemContentMedia";
+import type { ApiDrop } from "@/generated/models/ApiDrop";
+import { formatNumberWithCommas } from "@/helpers/Helpers";
+import { getScaledImageUri, ImageScale } from "@/helpers/image.helpers";
+import useDeviceInfo from "@/hooks/useDeviceInfo";
+import Image from "next/image";
+import Link from "next/link";
+import NowMintingStatsItem from "./NowMintingStatsItem";
+
+interface LatestDropNextMintSectionProps {
+ readonly drop: ApiDrop;
+}
+
+const formatDropTimestamp = (timestamp: number): string | null => {
+ const date = new Date(timestamp);
+ if (Number.isNaN(date.getTime())) {
+ return null;
+ }
+
+ const dateLabel = new Intl.DateTimeFormat(undefined, {
+ month: "short",
+ day: "numeric",
+ }).format(date);
+ const timeLabel = new Intl.DateTimeFormat(undefined, {
+ hour: "2-digit",
+ minute: "2-digit",
+ hour12: false,
+ }).format(date);
+
+ return `${dateLabel} · ${timeLabel}`;
+};
+
+export default function LatestDropNextMintSection({
+ drop,
+}: LatestDropNextMintSectionProps) {
+ const { hasTouchScreen } = useDeviceInfo();
+ const media = drop.parts[0]?.media[0];
+ const title =
+ drop.title ??
+ drop.metadata.find((m) => m.data_key === "title")?.data_value ??
+ "Untitled";
+ const author = drop.author;
+ const authorHandle = author.handle ?? author.primary_address;
+ const authorName = author.handle ?? "Anonymous";
+ const submittedAt = formatDropTimestamp(drop.created_at);
+ const description =
+ drop.metadata.find((m) => m.data_key === "description")?.data_value ?? null;
+ const nextMintStart = getNextMintStart();
+ const nextMintLabel = formatFullDateTime(nextMintStart, "local");
+
+ return (
+
+
+ Next Drop
+
+
+
+
+
+
+
+ {media ? (
+
+ ) : (
+
+
+ No image
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ NEXT MINT
+
+
+
+ {nextMintLabel}
+
+
+
+ {title}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {authorHandle ? (
+
+
+
+ {authorName}
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {drop.wave.picture && (
+
+ )}
+
+ {drop.wave.name}
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/home/now-minting/LatestDropSection.tsx b/components/home/now-minting/LatestDropSection.tsx
new file mode 100644
index 0000000000..50cf23935b
--- /dev/null
+++ b/components/home/now-minting/LatestDropSection.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { ManifoldClaimStatus } from "@/hooks/useManifoldClaim";
+import { useNextMintDrop } from "@/hooks/useNextMintDrop";
+import { useNowMintingStatus } from "@/hooks/useNowMintingStatus";
+import { shouldShowNextMintInLatestDrop } from "@/helpers/mint-visibility.helpers";
+import LatestDropNextMintSection from "./LatestDropNextMintSection";
+import NowMintingSection from "./NowMintingSection";
+
+export default function LatestDropSection() {
+ const { nft, isFetching, status, isStatusLoading } = useNowMintingStatus();
+ const {
+ nextMint,
+ waveId,
+ isFetching: isNextMintFetching,
+ isSettingsLoaded,
+ } = useNextMintDrop();
+
+ const isNextMintReady = isSettingsLoaded && (!waveId || !isNextMintFetching);
+ const isDecisionReady = !isFetching && !isStatusLoading && isNextMintReady;
+
+ if (!isDecisionReady) {
+ return
;
+ }
+
+ const shouldShowNextMint = shouldShowNextMintInLatestDrop({
+ isMintEnded: status === ManifoldClaimStatus.ENDED,
+ nextMintExists: !!nextMint,
+ });
+
+ if (shouldShowNextMint && nextMint) {
+ return
;
+ }
+
+ return
;
+}
diff --git a/components/home/now-minting/NowMintingSection.tsx b/components/home/now-minting/NowMintingSection.tsx
index ac9e13c1b7..6f81cf2388 100644
--- a/components/home/now-minting/NowMintingSection.tsx
+++ b/components/home/now-minting/NowMintingSection.tsx
@@ -1,12 +1,18 @@
"use client";
-import { useNowMinting } from "@/hooks/useNowMinting";
+import type { NFTWithMemesExtendedData } from "@/entities/INFT";
import NowMintingArtwork from "./NowMintingArtwork";
import NowMintingDetails from "./NowMintingDetails";
-export default function NowMintingSection() {
- const { nft, isFetching } = useNowMinting();
+interface NowMintingSectionProps {
+ readonly nft: NFTWithMemesExtendedData | undefined;
+ readonly isFetching: boolean;
+}
+export default function NowMintingSection({
+ nft,
+ isFetching,
+}: NowMintingSectionProps) {
if (isFetching && !nft) {
return (
diff --git a/components/home/now-minting/NowMintingStatsItem.tsx b/components/home/now-minting/NowMintingStatsItem.tsx
index 48402cec92..f8b3d3dbd8 100644
--- a/components/home/now-minting/NowMintingStatsItem.tsx
+++ b/components/home/now-minting/NowMintingStatsItem.tsx
@@ -5,6 +5,7 @@ interface NowMintingStatsItemProps {
readonly value?: ReactNode;
readonly status?: "active" | "upcoming" | "ended" | undefined;
readonly isLoading?: boolean;
+ readonly allowWrap?: boolean;
}
export default function NowMintingStatsItem({
@@ -12,6 +13,7 @@ export default function NowMintingStatsItem({
value,
status,
isLoading,
+ allowWrap,
}: NowMintingStatsItemProps) {
const getValueColor = () => {
if (status === "active") return "tw-text-emerald-400";
@@ -21,14 +23,16 @@ export default function NowMintingStatsItem({
return (
-
+
{label}
{isLoading ? (
) : (
{value}
diff --git a/components/home/now-minting/index.ts b/components/home/now-minting/index.ts
index 71527bb50c..490aeab653 100644
--- a/components/home/now-minting/index.ts
+++ b/components/home/now-minting/index.ts
@@ -1 +1 @@
-export { default as NowMintingSection } from "./NowMintingSection";
+export { default as LatestDropSection } from "./LatestDropSection";
diff --git a/helpers/mint-visibility.helpers.ts b/helpers/mint-visibility.helpers.ts
new file mode 100644
index 0000000000..f6dbb2baec
--- /dev/null
+++ b/helpers/mint-visibility.helpers.ts
@@ -0,0 +1,29 @@
+import { isMintEligibleUtcDay } from "@/components/meme-calendar/meme-calendar.helpers";
+
+function isMintingDayUtc(now: Date = new Date()): boolean {
+ return isMintEligibleUtcDay(now);
+}
+
+interface ShouldShowNextMintAfterEndParams {
+ readonly isMintEnded: boolean;
+ readonly nextMintExists: boolean;
+ readonly now?: Date;
+}
+
+export function shouldShowNextMintInLatestDrop({
+ isMintEnded,
+ nextMintExists,
+ now,
+}: ShouldShowNextMintAfterEndParams): boolean {
+ if (!isMintEnded || !nextMintExists) return false;
+ return !isMintingDayUtc(now);
+}
+
+export function shouldShowNextWinnerInComingUp({
+ isMintEnded,
+ nextMintExists,
+ now,
+}: ShouldShowNextMintAfterEndParams): boolean {
+ if (!nextMintExists) return false;
+ return !isMintEnded || isMintingDayUtc(now);
+}
diff --git a/hooks/useNextMintDrop.ts b/hooks/useNextMintDrop.ts
new file mode 100644
index 0000000000..fbba585181
--- /dev/null
+++ b/hooks/useNextMintDrop.ts
@@ -0,0 +1,39 @@
+import type { ApiDrop } from "@/generated/models/ApiDrop";
+import { useSeizeSettings } from "@/contexts/SeizeSettingsContext";
+import { useWaveDecisions } from "@/hooks/waves/useWaveDecisions";
+
+type NextMintDropState = {
+ readonly nextMint: ApiDrop | null;
+ readonly nextMintTitle: string | null;
+ readonly waveId: string | null;
+ readonly isReady: boolean;
+ readonly isSettingsLoaded: boolean;
+ readonly isFetching: boolean;
+};
+
+export const useNextMintDrop = (): NextMintDropState => {
+ const { seizeSettings, isLoaded } = useSeizeSettings();
+ const waveId = seizeSettings.memes_wave_id;
+
+ const { decisionPoints, isFetching } = useWaveDecisions({
+ waveId: waveId ?? "",
+ enabled: !!waveId,
+ });
+
+ const latestDecision = decisionPoints[decisionPoints.length - 1];
+ const nextMint = latestDecision?.winners[0]?.drop ?? null;
+
+ const nextMintTitle =
+ nextMint?.title ??
+ nextMint?.metadata.find((m) => m.data_key === "title")?.data_value ??
+ null;
+
+ return {
+ nextMint,
+ nextMintTitle,
+ waveId,
+ isReady: isLoaded && !!waveId,
+ isSettingsLoaded: isLoaded,
+ isFetching: isFetching || (!isLoaded && !!waveId),
+ };
+};
diff --git a/hooks/useNowMintingStatus.ts b/hooks/useNowMintingStatus.ts
new file mode 100644
index 0000000000..19f7786d6e
--- /dev/null
+++ b/hooks/useNowMintingStatus.ts
@@ -0,0 +1,25 @@
+import type { NFTWithMemesExtendedData } from "@/entities/INFT";
+import { useMemesManifoldClaim } from "@/hooks/useManifoldClaim";
+import { useNowMinting } from "@/hooks/useNowMinting";
+import type { ManifoldClaimStatus } from "@/hooks/useManifoldClaim";
+
+type NowMintingStatus = {
+ readonly nft: NFTWithMemesExtendedData | undefined;
+ readonly isFetching: boolean;
+ readonly status: ManifoldClaimStatus | null;
+ readonly isStatusLoading: boolean;
+ readonly error: unknown;
+};
+
+export const useNowMintingStatus = (): NowMintingStatus => {
+ const { nft, isFetching, error } = useNowMinting();
+ const claim = useMemesManifoldClaim(nft?.id ?? -1);
+
+ return {
+ nft,
+ isFetching,
+ status: claim?.status ?? null,
+ isStatusLoading: !!nft && !claim,
+ error,
+ };
+};