diff --git a/components/home/HomePageContent.tsx b/components/home/HomePageContent.tsx index 1d8e8af65d..1659f497d3 100644 --- a/components/home/HomePageContent.tsx +++ b/components/home/HomePageContent.tsx @@ -1,7 +1,7 @@ "use client"; import { HeroHeader } from "./hero"; -import { NowMintingSection } from "./now-minting"; +import { LatestDropSection } from "./now-minting"; import { NextMintLeadingSection } from "@/components/home/next-mint-leading/NextMintLeadingSection"; import { BoostedSection } from "@/components/home/boosted/BoostedSection"; import { ExploreWavesSection } from "@/components/home/explore-waves/ExploreWavesSection"; @@ -11,7 +11,7 @@ export default function HomePageContent() { return (
- +
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} + + + ) : ( +
+ + + {authorName} + +
+ )} +
+ +
+
+ + {drop.wave.picture && ( + {drop.wave.name} + )} + + {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, + }; +};