Skip to content
4 changes: 2 additions & 2 deletions components/home/HomePageContent.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -11,7 +11,7 @@ export default function HomePageContent() {
return (
<div className="tw-overflow-x-hidden tw-border-y-0 tw-border-l-0 tw-border-r tw-border-solid tw-border-iron-900">
<HeroHeader />
<NowMintingSection />
<LatestDropSection />
<HomePageTextSection />
<div className="tw-h-px tw-w-full tw-bg-[linear-gradient(90deg,transparent_0%,rgba(255,255,255,0.08)_15%,rgba(255,255,255,0.08)_85%,transparent_100%)]" />
<div className="tw-pt-10 md:tw-pt-16">
Expand Down
46 changes: 23 additions & 23 deletions components/home/next-mint-leading/NextMintLeadingSection.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -19,31 +20,25 @@ 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 ?? "",
sort: WaveDropsLeaderboardSort.RATING_PREDICTION,
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 =
Expand All @@ -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
Expand All @@ -61,7 +61,7 @@ export function NextMintLeadingSection() {
const isFetching =
isNowMintingFetching || isWinnersFetching || isLeaderboardFetching;

if (!isLoaded || !waveId) {
if (!isReady || !waveId) {
return null;
}

Expand Down
189 changes: 189 additions & 0 deletions components/home/now-minting/LatestDropNextMintSection.tsx
Original file line number Diff line number Diff line change
@@ -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;
Comment thread
simo6529 marked this conversation as resolved.
const nextMintStart = getNextMintStart();
const nextMintLabel = formatFullDateTime(nextMintStart, "local");

return (
<section className="tw-relative tw-z-50 tw-px-4 tw-pb-4 tw-pt-6 md:tw-px-6 md:tw-pb-8 md:tw-pt-10 lg:tw-px-8">
<span className="tw-mb-3 tw-block tw-text-xl tw-font-semibold tw-tracking-tight tw-text-iron-100 md:tw-mb-4 md:tw-text-2xl">
Next Drop
</span>

<div className="tw-relative tw-overflow-hidden tw-rounded-2xl tw-border tw-border-solid tw-border-white/[0.03] tw-bg-iron-950">
<div className="tw-grid tw-grid-cols-1 tw-items-center tw-gap-x-6 tw-gap-y-6 lg:tw-grid-cols-12 xl:tw-grid-cols-9">
<div className="tw-p-0 lg:tw-col-span-6 xl:tw-col-span-5">
<div className="tw-relative tw-flex tw-h-[clamp(360px,65vw,640px)] tw-w-full tw-items-center tw-justify-center tw-overflow-hidden tw-bg-black/50">
<div className="tw-[&>div]:tw-mx-0 tw-flex tw-h-full tw-w-full tw-items-center tw-justify-center">
{media ? (
<DropListItemContentMedia
media_mime_type={media.mime_type}
media_url={media.url}
imageObjectPosition="center"
imageScale={ImageScale.AUTOx600}
disableAutoPlay={hasTouchScreen}
disableModal={hasTouchScreen}
/>
) : (
<div className="tw-flex tw-size-full tw-items-center tw-justify-center tw-bg-black/40">
<span className="tw-text-sm tw-text-white/40">
No image
</span>
</div>
)}
</div>
</div>
</div>

<div className="tw-p-5 md:tw-p-6 lg:tw-col-span-6 xl:tw-col-span-4">
<div className="tw-flex tw-flex-col tw-gap-5">
<div className="tw-flex tw-flex-col">
<div className="tw-flex tw-flex-wrap tw-items-center tw-gap-2">
<span className="tw-size-1.5 tw-rounded-full tw-bg-emerald-500" />
<span className="tw-text-[11px] tw-font-semibold tw-uppercase tw-leading-5 tw-tracking-wide tw-text-emerald-400">
NEXT MINT
</span>
</div>
<span className="tw-mt-1 tw-font-mono tw-text-xs tw-text-white/50">
{nextMintLabel}
</span>

<Link
href={`/waves?wave=${drop.wave.id}&drop=${drop.id}`}
className="tw-mt-3 tw-text-xl tw-font-semibold tw-leading-tight tw-text-iron-50 tw-no-underline tw-transition-colors tw-duration-300 desktop-hover:hover:tw-text-iron-200 sm:tw-text-2xl md:tw-text-3xl"
>
{title}
</Link>

{description && (
<p className="tw-mt-3 tw-line-clamp-2 tw-text-sm tw-leading-relaxed tw-text-iron-300">
{description}
</p>
)}

{authorHandle ? (
<Link
href={`/${authorHandle}`}
className="tw-mt-3 tw-flex tw-min-w-0 tw-items-center tw-gap-2 tw-no-underline"
>
<ProfileAvatar
pfpUrl={author.pfp}
alt={authorHandle || authorName}
size={ProfileBadgeSize.SMALL}
/>
<span className="tw-min-w-0 tw-truncate tw-text-sm tw-text-iron-200 desktop-hover:hover:tw-text-iron-100">
{authorName}
</span>
</Link>
) : (
<div className="tw-mt-3 tw-flex tw-min-w-0 tw-items-center tw-gap-2">
<ProfileAvatar
pfpUrl={author.pfp}
alt={authorName}
size={ProfileBadgeSize.SMALL}
/>
<span className="tw-min-w-0 tw-truncate tw-text-sm tw-text-iron-200">
{authorName}
</span>
</div>
)}
</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">
<div className="tw-col-span-2">
<NowMintingStatsItem
label="Wave"
allowWrap
value={
<Link
href={`/waves?wave=${drop.wave.id}`}
className="tw-inline-flex tw-items-center tw-gap-2 tw-text-iron-100 tw-no-underline desktop-hover:hover:tw-text-iron-200"
>
{drop.wave.picture && (
<Image
src={getScaledImageUri(
drop.wave.picture,
ImageScale.W_AUTO_H_50
)}
alt={drop.wave.name}
width={20}
height={20}
className="tw-size-5 tw-shrink-0 tw-rounded-full tw-object-cover tw-ring-1 tw-ring-white/10"
/>
)}
<span className="tw-min-w-0 tw-break-words">
{drop.wave.name}
</span>
</Link>
}
/>
</div>
<NowMintingStatsItem
label="Submitted"
value={submittedAt ?? "—"}
/>
<NowMintingStatsItem
label="Rating"
value={`${formatNumberWithCommas(drop.rating)} TDH`}
/>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
36 changes: 36 additions & 0 deletions components/home/now-minting/LatestDropSection.tsx
Original file line number Diff line number Diff line change
@@ -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 <NowMintingSection nft={undefined} isFetching />;
}
Comment thread
simo6529 marked this conversation as resolved.

const shouldShowNextMint = shouldShowNextMintInLatestDrop({
isMintEnded: status === ManifoldClaimStatus.ENDED,
nextMintExists: !!nextMint,
});

if (shouldShowNextMint && nextMint) {
return <LatestDropNextMintSection drop={nextMint} />;
}

return <NowMintingSection nft={nft} isFetching={isFetching} />;
}
12 changes: 9 additions & 3 deletions components/home/now-minting/NowMintingSection.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="tw-px-4 tw-pb-4 tw-pt-6 md:tw-px-6 md:tw-pb-8 md:tw-pt-10 lg:tw-px-8">
Expand Down
8 changes: 6 additions & 2 deletions components/home/now-minting/NowMintingStatsItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ interface NowMintingStatsItemProps {
readonly value?: ReactNode;
readonly status?: "active" | "upcoming" | "ended" | undefined;
readonly isLoading?: boolean;
readonly allowWrap?: boolean;
}

export default function NowMintingStatsItem({
label,
value,
status,
isLoading,
allowWrap,
}: NowMintingStatsItemProps) {
const getValueColor = () => {
if (status === "active") return "tw-text-emerald-400";
Expand All @@ -21,14 +23,16 @@ export default function NowMintingStatsItem({

return (
<div className="tw-flex tw-flex-col tw-gap-1.5">
<span className="tw-text-xs tw-uppercase tw-tracking-wider tw-font-medium tw-text-iron-400">
<span className="tw-text-xs tw-font-medium tw-uppercase tw-tracking-wider tw-text-iron-400">
{label}
</span>
{isLoading ? (
<span className="tw-h-6 tw-w-20 tw-animate-pulse tw-rounded tw-bg-iron-800" />
) : (
<span
className={`tw-whitespace-nowrap tw-font-mono tw-text-sm tw-font-medium md:tw-text-base ${getValueColor()}`}
className={`tw-font-mono tw-text-sm tw-font-medium md:tw-text-base ${getValueColor()} ${
allowWrap ? "tw-whitespace-normal" : "tw-whitespace-nowrap"
}`}
>
{value}
</span>
Expand Down
2 changes: 1 addition & 1 deletion components/home/now-minting/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default as NowMintingSection } from "./NowMintingSection";
export { default as LatestDropSection } from "./LatestDropSection";
Loading