Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export default function DropListItemRateGiveSubmit({
max_rating: 0,
reaction: null,
boosted: false,
bookmarked: false,
};

draft.context_profile_context = {
Expand Down
5 changes: 3 additions & 2 deletions components/home/HomePageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import SubmissionCarouselSection from "./SubmissionCarouselSection";
import { HeroHeader } from "./hero";
import { NowMintingSection } from "./now-minting";
import { NextMintLeadingSection } from "./next-mint-leading";
import { BoostedSection } from "./boosted";
Expand All @@ -9,8 +9,9 @@ import { ExploreWavesSection } from "./explore-waves";
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 />
<SubmissionCarouselSection />

<div className="tw-mt-10 tw-border tw-border-x-0 tw-border-b-0 tw-border-t tw-border-solid tw-border-iron-800 tw-pt-10 md:tw-mt-16 md:tw-pt-16">
<NextMintLeadingSection />
<BoostedSection />
Expand Down
133 changes: 75 additions & 58 deletions components/home/boosted/BoostedDropCardHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ContentDisplay from "@/components/waves/drops/ContentDisplay";
import { buildProcessedContent } from "@/components/waves/drops/media-utils";
import type { ApiDrop } from "@/generated/models/ApiDrop";
import { getScaledImageUri, ImageScale } from "@/helpers/image.helpers";
import { getTimeAgoShort } from "@/helpers/Helpers";
import { LinkIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import { memo, useCallback, useMemo } from "react";
Expand All @@ -28,13 +29,14 @@ const BoostedDropCardHome = memo(
[part?.content, part?.media]
);

// Check if content is primarily a link
const urlRegex = /^https?:\/\/[^\s]+$/i;
// Check if content starts with a link (for icon display)
const textContent = part?.content ?? "";
const isLink = urlRegex.test(textContent.trim());
const textColorClass = isLink ? "tw-text-primary-300" : "tw-text-iron-300";
const startsWithLink = /^https?:\/\//i.test(textContent.trim());

const { author, wave, boosts } = drop;
const MAX_FIRE_ICONS = 5;
const fireIconsToShow = Math.min(boosts, MAX_FIRE_ICONS);
const remainingBoosts = boosts - MAX_FIRE_ICONS;
const waveName = wave.name;
const handleCardKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
Expand All @@ -53,23 +55,35 @@ const BoostedDropCardHome = memo(
tabIndex={0}
onClick={onClick}
onKeyDown={handleCardKeyDown}
className="hover:tw-border-50/10 tw-group tw-relative tw-flex tw-w-60 tw-flex-shrink-0 tw-cursor-pointer tw-flex-col tw-overflow-hidden tw-rounded-xl tw-border tw-border-solid tw-border-white/5 tw-bg-black tw-p-0 tw-text-left tw-transition-all tw-duration-500 tw-ease-out hover:tw--translate-y-1.5 hover:tw-shadow-[0_20px_40px_-15px_rgba(0,0,0,0.5)]"
className="hover:tw-border-50/10 tw-group tw-relative tw-flex tw-w-full tw-cursor-pointer tw-flex-col tw-overflow-hidden tw-rounded-xl tw-border tw-border-solid tw-border-white/5 tw-bg-black tw-p-0 tw-text-left tw-transition-all tw-duration-500 tw-ease-out hover:tw--translate-y-1.5 hover:tw-shadow-[0_0_40px_-5px_rgba(255,255,255,0.15)]"
>
{/* Inner Highlight (Glass Edge) */}
<div className="tw-pointer-events-none tw-absolute tw-inset-0 tw-z-20 tw-rounded-xl tw-border tw-border-solid tw-border-white/10" />

<div className="tw-absolute tw-right-3 tw-top-3 tw-z-10 tw-flex tw-items-center tw-gap-1.5 tw-rounded-full tw-border tw-border-solid tw-border-iron-50/5 tw-bg-iron-950/60 tw-px-2.5 tw-py-1 tw-shadow-lg tw-backdrop-blur-md">
<BoostIcon
className="tw-size-3 tw-flex-shrink-0 tw-text-orange-400"
variant="filled"
/>
<span className="tw-text-[10px] tw-font-medium tw-tabular-nums tw-text-iron-50">
{boosts}
{/* Time ago */}
<div className="tw-absolute tw-left-3 tw-top-3 tw-z-10 tw-rounded-full tw-border tw-border-solid tw-border-iron-50/5 tw-bg-iron-950/60 tw-px-2.5 tw-py-1 tw-shadow-lg tw-backdrop-blur-md">
<span className="tw-text-[10px] tw-text-iron-400">
{getTimeAgoShort(drop.created_at)}
</span>
</div>

<div className="tw-relative tw-aspect-[4/5] tw-w-full tw-overflow-hidden tw-rounded-xl sm:tw-aspect-[3/4]">
{media ? (
<div className="tw-absolute tw-right-3 tw-top-3 tw-z-10 tw-flex tw-items-center tw-gap-0.5 tw-rounded-full tw-border tw-border-solid tw-border-iron-50/5 tw-bg-iron-950/60 tw-px-2.5 tw-py-1 tw-shadow-lg tw-backdrop-blur-md">
{Array.from({ length: fireIconsToShow }).map((_, i) => (
<BoostIcon
key={i}
className="tw-size-3 tw-flex-shrink-0 tw-text-orange-400"
variant="filled"
/>
))}
{remainingBoosts > 0 && (
<span className="tw-ml-1 tw-text-[10px] tw-font-medium tw-tabular-nums tw-text-iron-50">
+{remainingBoosts}
</span>
)}
</div>

{media ? (
<div className="tw-relative tw-aspect-[8/5] tw-w-full tw-overflow-hidden tw-rounded-xl">
<div className="tw-relative tw-h-full tw-w-full">
{/* Glow effect */}
<div
Expand All @@ -92,54 +106,57 @@ const BoostedDropCardHome = memo(
</div>
</div>
</div>
) : (
<div className="tw-relative tw-flex tw-size-full tw-items-center tw-justify-center tw-p-6">
{isLink ? (
<LinkIcon className="tw-absolute tw-left-6 tw-top-8 tw-size-6 tw-text-iron-700 tw-opacity-50" />
) : (
<span className="tw-absolute tw-left-6 tw-top-8 tw-select-none tw-font-serif tw-text-6xl tw-leading-none tw-text-iron-800 tw-opacity-50">
{"\u201C"}
</span>
)}
<div className="tw-relative tw-z-10 tw-w-full">
<ContentDisplay
content={previewContent}
className={`tw-flex tw-w-full tw-flex-col tw-items-center tw-gap-1 tw-whitespace-pre-wrap tw-break-words tw-text-center tw-font-serif tw-text-md tw-font-normal tw-leading-relaxed ${textColorClass}`}
textClassName="tw-line-clamp-6 tw-w-full tw-whitespace-pre-wrap tw-break-words tw-text-center tw-leading-relaxed"
/>
</div>
</div>
)}
</div>

<div className="tw-relative tw-z-10 tw-flex tw-items-center tw-gap-3 tw-border-x-0 tw-border-b-0 tw-border-t tw-border-solid tw-border-iron-900 tw-bg-black tw-px-4 tw-py-4">
<ProfileAvatar
pfpUrl={author.pfp}
alt={author.handle ?? "User"}
size={ProfileBadgeSize.SMALL}
/>
<div className="tw-flex tw-min-w-0 tw-flex-col">
{author.handle ? (
<Link
href={`/${author.handle}`}
onClick={(event) => event.stopPropagation()}
className="tw-truncate tw-text-sm tw-font-medium tw-text-iron-50 tw-no-underline tw-transition-colors desktop-hover:hover:tw-text-white"
>
{author.handle}
</Link>
</div>
) : (
<div className="tw-relative tw-flex tw-aspect-[8/5] tw-w-full tw-items-center tw-justify-center tw-overflow-hidden tw-rounded-xl tw-p-6">
{startsWithLink ? (
<LinkIcon className="tw-absolute tw-left-6 tw-top-8 tw-size-6 tw-text-iron-700 tw-opacity-50" />
) : (
<span className="tw-truncate tw-text-sm tw-font-medium tw-text-iron-50 tw-transition-colors group-hover:tw-text-white">
Anonymous
<span className="tw-absolute tw-left-6 tw-top-8 tw-select-none tw-font-serif tw-text-6xl tw-leading-none tw-text-iron-800 tw-opacity-50">
{"\u201C"}
</span>
)}
<Link
href={`/waves?wave=${wave.id}`}
onClick={(event) => event.stopPropagation()}
className="tw-truncate tw-text-xs tw-text-iron-500 tw-no-underline tw-transition-colors desktop-hover:hover:tw-text-iron-300"
>
{waveName}
</Link>
<ContentDisplay
content={previewContent}
shouldClamp={false}
className="tw-flex tw-w-full tw-flex-col tw-items-center tw-gap-1 tw-whitespace-pre-wrap tw-break-words tw-text-center tw-font-serif tw-text-lg tw-font-normal tw-leading-relaxed tw-text-iron-300"
textClassName="tw-line-clamp-6 tw-w-full tw-whitespace-pre-wrap tw-break-words tw-text-center tw-leading-relaxed"
/>
</div>
)}

<div className="tw-relative tw-z-10 tw-flex tw-items-center tw-justify-between tw-border-x-0 tw-border-b-0 tw-border-t tw-border-solid tw-border-iron-900 tw-bg-black tw-px-4 tw-py-3">
{/* Author */}
<Link
href={author.handle ? `/${author.handle}` : "#"}
onClick={(event) => event.stopPropagation()}
className="tw-flex tw-items-center tw-gap-2 tw-no-underline tw-transition-opacity desktop-hover:hover:tw-opacity-80"
>
<ProfileAvatar
pfpUrl={author.pfp}
alt={author.handle ?? "User"}
size={ProfileBadgeSize.SMALL}
/>
<span className="tw-text-sm tw-font-medium tw-text-iron-50">
{author.handle ?? "Anonymous"}
</span>
</Link>
Comment thread
simo6529 marked this conversation as resolved.

{/* Wave */}
<Link
href={`/waves?wave=${wave.id}`}
onClick={(event) => event.stopPropagation()}
className="tw-group/wave tw-flex tw-items-center tw-gap-2 tw-no-underline tw-transition-all"
>
<span className="tw-text-xs tw-text-iron-500 tw-transition-colors desktop-hover:group-hover/wave:tw-text-iron-300 desktop-hover:group-hover/wave:tw-underline">{waveName}</span>
{wave.picture && (
<img
src={getScaledImageUri(wave.picture, ImageScale.W_AUTO_H_50)}
alt={waveName}
className="tw-size-6 tw-rounded-md tw-object-cover tw-ring-1 tw-ring-transparent tw-transition-all desktop-hover:group-hover/wave:tw-ring-iron-600"
/>
)}
</Link>
</div>
</div>
);
Expand Down
122 changes: 46 additions & 76 deletions components/home/boosted/BoostedSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,43 @@

import type { ApiDrop } from "@/generated/models/ApiDrop";
import { useBoostedDrops } from "@/hooks/useBoostedDrops";
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
import { useMediaQuery } from "@/hooks/useMediaQuery";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useMemo } from "react";
import Masonry from "react-masonry-css";
import BoostedDropCardHome from "./BoostedDropCardHome";

const BOOSTED_DROPS_LIMIT = 50;
const MAX_ROWS = 3;

const MASONRY_BREAKPOINTS = {
default: 4,
1280: 3,
1024: 3,
768: 2,
640: 1,
};

export function BoostedSection() {
const router = useRouter();
const scrollRef = useRef<HTMLDivElement>(null);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
const { data: drops, isLoading } = useBoostedDrops({
limit: BOOSTED_DROPS_LIMIT,
});

const updateScrollState = useCallback(() => {
const container = scrollRef.current;
if (!container) return;
const maxScrollLeft = container.scrollWidth - container.clientWidth;
const epsilon = 1;
setCanScrollLeft(container.scrollLeft > epsilon);
setCanScrollRight(container.scrollLeft < maxScrollLeft - epsilon);
}, []);
// Detect breakpoints to determine column count
const isXl = useMediaQuery("(min-width: 1281px)");
const isLg = useMediaQuery("(min-width: 1024px)");
const isMd = useMediaQuery("(min-width: 768px)");
const isSm = useMediaQuery("(min-width: 640px)");

useEffect(() => {
const container = scrollRef.current;
if (!container) return;
updateScrollState();
const handleScroll = () => updateScrollState();
container.addEventListener("scroll", handleScroll, { passive: true });
window.addEventListener("resize", updateScrollState);
return () => {
container.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", updateScrollState);
};
}, [updateScrollState, drops?.length]);
// Calculate max items based on columns * MAX_ROWS (mobile: 1x6)
const visibleDrops = useMemo(() => {
if (!drops) return [];
if (!isSm) return drops.slice(0, 6); // Mobile: 1 col × 6 items
const columns = isXl ? 4 : isLg ? 3 : 2;
const maxItems = columns * MAX_ROWS;
return drops.slice(0, maxItems);
}, [drops, isXl, isLg, isMd, isSm]);
Comment thread
simo6529 marked this conversation as resolved.

const handleDropClick = useCallback(
(drop: ApiDrop) => {
Expand All @@ -47,15 +47,6 @@ export function BoostedSection() {
[router]
);

const scroll = (direction: "left" | "right") => {
if (!scrollRef.current) return;
const scrollAmount = 260;
scrollRef.current.scrollBy({
left: direction === "left" ? -scrollAmount : scrollAmount,
behavior: "smooth",
});
};

if (isLoading) {
return (
<section className="-tw-mx-8 tw-border-x-0 tw-border-y tw-border-solid tw-border-iron-900 tw-bg-iron-950 tw-px-4 tw-py-16 md:tw-px-6 lg:tw-px-8">
Expand All @@ -68,57 +59,36 @@ export function BoostedSection() {
);
}

if (!drops || drops.length === 0) {
if (!drops || visibleDrops.length === 0) {
return null;
}

return (
<section className="-tw-mx-8 tw-border-x-0 tw-border-y tw-border-solid tw-border-iron-900 tw-bg-iron-950 tw-px-4 tw-py-10 md:tw-py-16 md:tw-px-6 lg:tw-px-8">
<section className="-tw-mx-8 tw-border-x-0 tw-border-y tw-border-solid tw-border-iron-900 tw-bg-iron-950 tw-px-4 tw-py-10 md:tw-px-6 md:tw-py-16 lg:tw-px-8">
<div className="tw-px-8">
<div className="tw-mb-8 tw-flex tw-flex-col tw-items-start tw-gap-4 sm:tw-flex-row sm:tw-items-end sm:tw-justify-between">
<div>
<span className="tw-m-0 tw-text-xl tw-font-semibold tw-tracking-tight tw-text-white md:tw-text-2xl">
Boosted Drops
</span>
<p className="tw-mb-0 tw-mt-2 tw-text-base tw-text-iron-500">
Community-boosted right now
</p>
</div>
<div className="tw-flex tw-items-center tw-gap-2">
<button
type="button"
onClick={() => scroll("left")}
disabled={!canScrollLeft}
className="tw-flex tw-h-11 tw-w-11 tw-items-center tw-justify-center tw-rounded-full tw-border-none tw-bg-iron-900 tw-text-iron-500 tw-shadow-lg tw-ring-1 tw-ring-iron-50/5 tw-transition-all hover:tw-scale-105 hover:tw-bg-iron-800 hover:tw-text-iron-50 active:tw-scale-95 disabled:tw-cursor-not-allowed disabled:tw-opacity-40 disabled:hover:tw-scale-100 disabled:hover:tw-bg-iron-900 disabled:hover:tw-text-iron-500"
aria-label="Scroll left"
>
<ChevronLeftIcon className="tw-size-4 tw-flex-shrink-0" />
</button>
<button
type="button"
onClick={() => scroll("right")}
disabled={!canScrollRight}
className="tw-flex tw-h-11 tw-w-11 tw-items-center tw-justify-center tw-rounded-full tw-border-none tw-bg-iron-900 tw-text-iron-500 tw-shadow-lg tw-ring-1 tw-ring-iron-50/5 tw-transition-all hover:tw-scale-105 hover:tw-bg-iron-800 hover:tw-text-iron-50 active:tw-scale-95 disabled:tw-cursor-not-allowed disabled:tw-opacity-40 disabled:hover:tw-scale-100 disabled:hover:tw-bg-iron-900 disabled:hover:tw-text-iron-500"
aria-label="Scroll right"
>
<ChevronRightIcon className="tw-size-4" />
</button>
</div>
<div className="tw-mb-8">
<span className="tw-m-0 tw-text-xl tw-font-semibold tw-tracking-tight tw-text-white md:tw-text-2xl">
Boosted Drops
</span>
<p className="tw-mb-0 tw-mt-2 tw-text-base tw-text-iron-500">
Community-boosted right now
</p>
</div>

{/* Horizontal scroll container */}
<div
ref={scrollRef}
className="-tw-mx-6 tw-flex tw-gap-5 tw-overflow-x-auto tw-scroll-smooth tw-px-6 tw-pb-8 tw-pt-4 tw-scrollbar-none md:-tw-mx-8 md:tw-px-8"
<Masonry
breakpointCols={MASONRY_BREAKPOINTS}
className="tw--ml-5 tw-flex tw-w-auto"
columnClassName="tw-pl-5 tw-bg-clip-padding"
>
{drops.map((drop) => (
<BoostedDropCardHome
key={drop.id}
drop={drop}
onClick={() => handleDropClick(drop)}
/>
{visibleDrops.map((drop) => (
<div key={drop.id} className="tw-mb-5">
<BoostedDropCardHome
drop={drop}
onClick={() => handleDropClick(drop)}
/>
</div>
))}
</div>
</Masonry>
</div>
</section>
);
Expand Down
Loading